Initial commit
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
from edgar.ownership.html_render import ownership_to_html
|
||||
from edgar.ownership.ownershipforms import (
|
||||
Address,
|
||||
DerivativeHolding,
|
||||
DerivativeHoldings,
|
||||
DerivativeTransaction,
|
||||
DerivativeTransactions,
|
||||
Footnotes,
|
||||
Form3,
|
||||
Form4,
|
||||
Form5,
|
||||
Issuer,
|
||||
NonDerivativeHolding,
|
||||
NonDerivativeHoldings,
|
||||
NonDerivativeTransaction,
|
||||
NonDerivativeTransactions,
|
||||
Owner,
|
||||
Ownership,
|
||||
OwnerSignature,
|
||||
PostTransactionAmounts,
|
||||
ReportingOwners,
|
||||
ReportingRelationship,
|
||||
TransactionCode,
|
||||
TransactionSummary,
|
||||
translate_ownership,
|
||||
)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
198
venv/lib/python3.10/site-packages/edgar/ownership/core.py
Normal file
198
venv/lib/python3.10/site-packages/edgar/ownership/core.py
Normal file
@@ -0,0 +1,198 @@
|
||||
import re
|
||||
from decimal import Decimal
|
||||
from typing import Any, Optional, Tuple, Union
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from lxml import etree
|
||||
|
||||
__all__ = ['is_numeric', 'compute_average_price', 'compute_total_value',
|
||||
'format_currency', 'format_amount', 'safe_numeric', 'format_numeric']
|
||||
|
||||
|
||||
def is_numeric(series: pd.Series) -> bool:
|
||||
if np.issubdtype(series.dtype, np.number):
|
||||
return True
|
||||
try:
|
||||
series.astype(float)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
def compute_average_price(shares: pd.Series, price: pd.Series) -> Decimal:
|
||||
"""
|
||||
Compute the average price of the trades
|
||||
:param shares: The number of shares as a series
|
||||
:param price: The price per share as a series
|
||||
:return:
|
||||
"""
|
||||
if is_numeric(shares) and is_numeric(price):
|
||||
shares = pd.to_numeric(shares)
|
||||
price = pd.to_numeric(price)
|
||||
value = (shares * price).sum() / shares.sum()
|
||||
return Decimal(str(value)).quantize(Decimal('0.01'))
|
||||
|
||||
|
||||
def compute_total_value(shares: pd.Series, price: pd.Series) -> Decimal:
|
||||
"""
|
||||
Compute the total value of the trades
|
||||
:param shares: The number of shares as a series
|
||||
:param price: The price per share as a series
|
||||
:return:
|
||||
"""
|
||||
if is_numeric(shares) and is_numeric(price):
|
||||
shares = pd.to_numeric(shares)
|
||||
price = pd.to_numeric(price)
|
||||
value = (shares * price).sum()
|
||||
return Decimal(str(value)).quantize(Decimal('0.01'))
|
||||
|
||||
def format_currency(amount: Union[int, float]) -> str:
|
||||
if amount is None or np.isnan(amount):
|
||||
return ""
|
||||
if isinstance(amount, (int, float)):
|
||||
return f"${amount:,.2f}"
|
||||
return str(amount)
|
||||
|
||||
|
||||
def format_amount(amount: Union[int, float]) -> str:
|
||||
if amount is None:
|
||||
return ""
|
||||
if isinstance(amount, (int, float)):
|
||||
# Can it be formatted as an integer?
|
||||
if amount == int(amount):
|
||||
return f"{amount:,.0f}"
|
||||
return f"{amount:,.2f}"
|
||||
return str(amount)
|
||||
|
||||
|
||||
def safe_numeric(value: Any) -> Optional[Union[int, float]]:
|
||||
"""
|
||||
Safely convert a value to a number, handling footnote references and other special cases.
|
||||
|
||||
Args:
|
||||
value: The value to convert (could be string, int, float, or None)
|
||||
|
||||
Returns:
|
||||
Numeric value if conversion is possible, None otherwise
|
||||
"""
|
||||
if value is None or pd.isna(value):
|
||||
return None
|
||||
|
||||
# If already a number, return as is
|
||||
if isinstance(value, (int, float)) and not np.isnan(value):
|
||||
return value
|
||||
|
||||
# Handle string cases
|
||||
if isinstance(value, str):
|
||||
# Remove commas, dollar signs, and whitespace
|
||||
cleaned = value.replace(',', '').replace('$', '').strip()
|
||||
|
||||
# Remove footnote references like [F1], [1], etc.
|
||||
cleaned = re.sub(r'\[\w+]', '', cleaned)
|
||||
|
||||
# Try numeric conversion
|
||||
try:
|
||||
# Check if it's an integer
|
||||
if '.' not in cleaned:
|
||||
return int(cleaned)
|
||||
else:
|
||||
return float(cleaned)
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def format_numeric(value: Any, currency: bool = False, default: str = "N/A") -> str:
|
||||
"""
|
||||
Format a potentially non-numeric value for display, handling special cases.
|
||||
|
||||
Args:
|
||||
value: The value to format
|
||||
currency: Whether to format as currency with $ symbol
|
||||
default: Default string to return if value can't be converted to number
|
||||
|
||||
Returns:
|
||||
Formatted string representation
|
||||
"""
|
||||
number = safe_numeric(value)
|
||||
|
||||
if number is None:
|
||||
# If the original value was a string with content, return it instead of default
|
||||
if isinstance(value, str) and value.strip():
|
||||
return value.strip()
|
||||
return default
|
||||
|
||||
if currency:
|
||||
return f"${number:,.2f}"
|
||||
else:
|
||||
# Format integers without decimal places
|
||||
if isinstance(number, int) or (isinstance(number, float) and number.is_integer()):
|
||||
return f"{int(number):,}"
|
||||
return f"{number:,.2f}"
|
||||
|
||||
# Add a dedicated currency formatter
|
||||
def format_price(value: Any, default: str = "N/A") -> str:
|
||||
"""Format a price value with currency symbol"""
|
||||
return format_numeric(value, currency=True, default=default)
|
||||
|
||||
|
||||
def _get_xml_value(elem: etree._Element, xpath: str, with_footnote: bool = True) -> str:
|
||||
"""Helper to safely get text from XML element with value tag and optional footnote
|
||||
|
||||
Args:
|
||||
elem: XML element to search within
|
||||
xpath: XPath expression to find target element
|
||||
with_footnote: Whether to include footnote references in output
|
||||
|
||||
Returns:
|
||||
Text value with optional footnote reference
|
||||
"""
|
||||
# Handle both direct text elements and those with <value> wrapper
|
||||
nodes = elem.xpath(xpath)
|
||||
if not nodes:
|
||||
return ''
|
||||
|
||||
node = nodes[0]
|
||||
|
||||
# Try to get text from <value> child first
|
||||
value = node.find('value')
|
||||
if value is not None:
|
||||
text = value.text if value.text else ''
|
||||
else:
|
||||
# If no <value> tag, get direct text content
|
||||
text = node.text if node.text else ''
|
||||
|
||||
# Add footnote reference if present and requested
|
||||
if with_footnote:
|
||||
footnote = node.find('footnoteId')
|
||||
if footnote is not None:
|
||||
footnote_id = footnote.get('id', '')
|
||||
if footnote_id:
|
||||
text = f"{text}<sup>({footnote_id})</sup>"
|
||||
|
||||
return text.strip()
|
||||
|
||||
|
||||
def _parse_name(name: str) -> Tuple[str, str, str]:
|
||||
"""Parse a full name into (last, first, middle) components"""
|
||||
# Remove any extra whitespace
|
||||
name = ' '.join(name.split())
|
||||
parts = name.split(' ')
|
||||
|
||||
if len(parts) == 1:
|
||||
return (parts[0], '', '')
|
||||
elif len(parts) == 2:
|
||||
return (parts[1], parts[0], '')
|
||||
else:
|
||||
return (parts[-1], parts[0], ' '.join(parts[1:-1]))
|
||||
|
||||
def _format_xml_date(date_str: str) -> str:
|
||||
"""Format YYYY-MM-DD to MM/DD/YYYY"""
|
||||
if not date_str:
|
||||
return ''
|
||||
try:
|
||||
year, month, day = date_str.split('-')
|
||||
return f"{month}/{day}/{year}"
|
||||
except ValueError:
|
||||
return date_str
|
||||
390
venv/lib/python3.10/site-packages/edgar/ownership/html_render.py
Normal file
390
venv/lib/python3.10/site-packages/edgar/ownership/html_render.py
Normal file
@@ -0,0 +1,390 @@
|
||||
# html_render.py - HTML rendering module for ownership forms
|
||||
|
||||
import os
|
||||
import re
|
||||
from typing import TYPE_CHECKING, Any, Optional
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from edgar.ownership.core import Ownership
|
||||
|
||||
import pandas as pd
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
|
||||
from edgar.ownership.core import format_numeric, format_price
|
||||
|
||||
|
||||
def _format_date(date_str: str) -> str:
|
||||
if not date_str or pd.isna(date_str):
|
||||
return "N/A"
|
||||
try:
|
||||
return pd.to_datetime(date_str).strftime('%m/%d/%Y')
|
||||
except (ValueError, TypeError):
|
||||
return str(date_str) # Return original if parsing fails
|
||||
|
||||
def _escape_html(value: Any) -> str:
|
||||
"""Escape HTML special characters in a string."""
|
||||
if value is None or (isinstance(value, float) and pd.isna(value)):
|
||||
return "N/A"
|
||||
s_val = str(value)
|
||||
# Only escape HTML special characters (Jinja2 will handle this automatically in templates)
|
||||
# but we still need it for our cell content preparation
|
||||
return s_val.replace('&', '&').replace('<', '<').replace('>', '>')
|
||||
|
||||
def format_owner_name(owner_name: Optional[str]) -> str:
|
||||
"""Format owner name for display."""
|
||||
if not owner_name:
|
||||
return ""
|
||||
return _escape_html(owner_name)
|
||||
|
||||
def format_address(street1: Optional[str], street2: Optional[str], city: Optional[str], state: Optional[str], zip_code: Optional[str]) -> str:
|
||||
"""Format address components into a HTML address string."""
|
||||
parts = []
|
||||
if street1:
|
||||
parts.append(_escape_html(street1))
|
||||
if street2:
|
||||
parts.append(_escape_html(street2))
|
||||
|
||||
city_state_zip_line_parts = []
|
||||
if city:
|
||||
city_state_zip_line_parts.append(_escape_html(city))
|
||||
if state:
|
||||
city_state_zip_line_parts.append(_escape_html(state))
|
||||
if zip_code:
|
||||
city_state_zip_line_parts.append(_escape_html(zip_code))
|
||||
|
||||
if city_state_zip_line_parts:
|
||||
parts.append(" ".join(city_state_zip_line_parts).strip())
|
||||
|
||||
return "<br>".join(part for part in parts if part)
|
||||
|
||||
|
||||
def ownership_to_html(ownership: 'Ownership') -> str:
|
||||
"""Convert an Ownership object to HTML format matching official SEC layout.
|
||||
|
||||
Args:
|
||||
ownership: Ownership object containing SEC form data
|
||||
|
||||
Returns:
|
||||
HTML string representation
|
||||
"""
|
||||
# Set up Jinja2 environment
|
||||
template_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'templates')
|
||||
env = Environment(loader=FileSystemLoader(template_dir))
|
||||
template = env.get_template('ownership_form.html')
|
||||
|
||||
# Extract basic information and prepare context
|
||||
form_type = ownership.form
|
||||
|
||||
# Prepare context dictionary for template rendering
|
||||
context = {'form_type': form_type, 'html_title': f"SEC Form {form_type}", 'form_name_display': f"FORM {form_type}",
|
||||
'form_title_display': {
|
||||
'3': "INITIAL STATEMENT OF BENEFICIAL OWNERSHIP OF SECURITIES",
|
||||
'4': "STATEMENT OF CHANGES IN BENEFICIAL OWNERSHIP",
|
||||
'5': "ANNUAL STATEMENT OF CHANGES IN BENEFICIAL OWNERSHIP"
|
||||
}.get(form_type, "STATEMENT OF OWNERSHIP"),
|
||||
'issuer_name': _escape_html(ownership.issuer.name if ownership.issuer else "N/A"),
|
||||
'ticker': _escape_html(ownership.issuer.ticker if ownership.issuer else "N/A"),
|
||||
'reporting_period': _format_date(ownership.reporting_period) if ownership.reporting_period else ''}
|
||||
|
||||
# Issuer information
|
||||
|
||||
# Reporting owner information
|
||||
reporting_owner = ownership.reporting_owners.owners[0] if ownership.reporting_owners.owners else None
|
||||
if reporting_owner:
|
||||
context['reporting_owner_name_str'] = format_owner_name(reporting_owner.name)
|
||||
context['reporting_owner_address_str'] = format_address(
|
||||
reporting_owner.address.street1 if reporting_owner.address else None,
|
||||
reporting_owner.address.street2 if reporting_owner.address else None,
|
||||
reporting_owner.address.city if reporting_owner.address else None,
|
||||
reporting_owner.address.state_or_country if reporting_owner.address else None,
|
||||
reporting_owner.address.zipcode if reporting_owner.address else None
|
||||
)
|
||||
|
||||
# Reporting owner relationship
|
||||
context['is_director'] = 'X' if reporting_owner.is_director else ''
|
||||
context['is_officer'] = 'X' if reporting_owner.is_officer else ''
|
||||
context['is_ten_pct'] = 'X' if reporting_owner.is_ten_pct_owner else ''
|
||||
context['is_other'] = 'X' if reporting_owner.is_other else ''
|
||||
context['officer_title'] = _escape_html(reporting_owner.officer_title) if reporting_owner.officer_title else ''
|
||||
|
||||
# Remarks
|
||||
context['remarks'] = _escape_html(ownership.remarks) if ownership.remarks else ''
|
||||
|
||||
# Footnotes
|
||||
if ownership.footnotes and ownership.footnotes._footnotes:
|
||||
footnotes_list = []
|
||||
for footnote in ownership.footnotes._footnotes:
|
||||
footnotes_list.append(f"<p>{_escape_html(footnote)}</p>")
|
||||
context['footnotes_html'] = "\n".join(footnotes_list)
|
||||
else:
|
||||
context['footnotes_html'] = ''
|
||||
|
||||
# Signature
|
||||
if ownership.signatures and ownership.signatures.signatures:
|
||||
first_signature = ownership.signatures.signatures[0]
|
||||
if first_signature.signature:
|
||||
context['sig_name'] = _escape_html(first_signature.signature)
|
||||
|
||||
if first_signature.date:
|
||||
if isinstance(first_signature.date, str):
|
||||
context['sig_date'] = _escape_html(_format_date(first_signature.date))
|
||||
else:
|
||||
context['sig_date'] = _escape_html(str(first_signature.date))
|
||||
|
||||
# Process Table I - Non-Derivative Securities
|
||||
non_deriv_rows = []
|
||||
non_derivative_table = getattr(ownership, 'non_derivative_table', None)
|
||||
if non_derivative_table:
|
||||
# Form 3 - Initial holdings
|
||||
if form_type == '3' and hasattr(non_derivative_table, 'holdings') and non_derivative_table.holdings is not None and not non_derivative_table.holdings.empty:
|
||||
for holding_tuple in non_derivative_table.holdings.data.itertuples(index=False):
|
||||
# Convert tuple to dictionary for easier access with default values
|
||||
holding = {col: getattr(holding_tuple, col, '') for col in holding_tuple._fields}
|
||||
|
||||
# Extract values using dictionary get() with defaults
|
||||
security_title = str(holding.get('Security', ''))
|
||||
shares_owned = format_numeric(str(holding.get('Shares', '')))
|
||||
direct_indirect_code = str(holding.get('DirectIndirect', ''))
|
||||
nature_of_ownership = str(holding.get('NatureOfOwnership', ''))
|
||||
|
||||
# Clean footnotes from values for table display
|
||||
security_title_clean = re.sub(r'<[Ss][Uu][Pp]>.*?</[Ss][Uu][Pp]>', '', security_title).strip()
|
||||
|
||||
row = [
|
||||
f'<td class="TableCell">{security_title_clean}</td>',
|
||||
f'<td class="TableCell Right">{shares_owned}</td>',
|
||||
f'<td class="TableCell Centered">{direct_indirect_code}</td>',
|
||||
f'<td class="TableCell">{nature_of_ownership}</td>'
|
||||
]
|
||||
non_deriv_rows.append(row)
|
||||
|
||||
# Form 4/5 - Transactions
|
||||
elif form_type in ['4', '5'] and hasattr(non_derivative_table, 'transactions') and non_derivative_table.transactions is not None and not non_derivative_table.transactions.empty:
|
||||
for transaction_tuple in non_derivative_table.transactions.data.itertuples(index=False):
|
||||
# Convert tuple to dictionary for easier access with default values
|
||||
transaction = {col: getattr(transaction_tuple, col, '') for col in transaction_tuple._fields}
|
||||
|
||||
# Extract values using dictionary get() with defaults
|
||||
security_title = str(transaction.get('Security', ''))
|
||||
transaction_date = _format_date(transaction.get('Date', ''))
|
||||
deemed_date = _format_date(transaction.get('DeemedDate', ''))
|
||||
transaction_code = str(transaction.get('Code', ''))
|
||||
transaction_v = str(transaction.get('V', ''))
|
||||
shares = format_numeric(str(transaction.get('Shares', '')))
|
||||
acquired_disposed = str(transaction.get('AcquiredDisposed', ''))
|
||||
price = format_price(transaction.get('Price', ''))
|
||||
owned_after_transaction = format_numeric(str(transaction.get('Remaining', '')))
|
||||
direct_indirect_code = str(transaction.get('DirectIndirect', ''))
|
||||
nature_of_ownership = str(transaction.get('NatureOfOwnership', ''))
|
||||
|
||||
# Clean footnotes from values for table display
|
||||
security_title_clean = re.sub(r'<[Ss][Uu][Pp]>.*?</[Ss][Uu][Pp]>', '', security_title).strip()
|
||||
|
||||
row = [
|
||||
f'<td class="TableCell">{security_title_clean}</td>',
|
||||
f'<td class="TableCell">{transaction_date}</td>',
|
||||
f'<td class="TableCell">{deemed_date}</td>',
|
||||
f'<td class="TableCell">{transaction_code}</td>',
|
||||
f'<td class="TableCell">{transaction_v}</td>',
|
||||
f'<td class="TableCell Right">{shares}</td>',
|
||||
f'<td class="TableCell Centered">{acquired_disposed}</td>',
|
||||
f'<td class="TableCell Right">{price}</td>',
|
||||
f'<td class="TableCell Right">{owned_after_transaction}</td>',
|
||||
f'<td class="TableCell Centered">{direct_indirect_code}</td>',
|
||||
f'<td class="TableCell">{nature_of_ownership}</td>'
|
||||
]
|
||||
non_deriv_rows.append(row)
|
||||
|
||||
# Process Holdings for Forms 4/5
|
||||
if hasattr(non_derivative_table, 'holdings') and non_derivative_table.holdings is not None and not non_derivative_table.holdings.empty:
|
||||
for holding_tuple in non_derivative_table.holdings.data.itertuples(index=False):
|
||||
# Convert tuple to dictionary for easier access with default values
|
||||
holding = {col: getattr(holding_tuple, col, '') for col in holding_tuple._fields}
|
||||
|
||||
# Extract values using dictionary get() with defaults
|
||||
security_title = str(holding.get('Security', ''))
|
||||
shares_owned = format_numeric(str(holding.get('Shares', '')))
|
||||
direct_indirect_code = str(holding.get('DirectIndirect', ''))
|
||||
nature_of_ownership = str(holding.get('NatureOfOwnership', ''))
|
||||
|
||||
# Clean footnotes from values for table display
|
||||
security_title_clean = re.sub(r'<[Ss][Uu][Pp]>.*?</[Ss][Uu][Pp]>', '', security_title).strip()
|
||||
|
||||
row = [
|
||||
f'<td class="TableCell">{security_title_clean}</td>',
|
||||
'<td class="TableCell"></td>', # Transaction Date
|
||||
'<td class="TableCell"></td>', # Deemed Execution Date
|
||||
'<td class="TableCell"></td>', # Transaction Code
|
||||
'<td class="TableCell"></td>', # V
|
||||
'<td class="TableCell"></td>', # Amount
|
||||
'<td class="TableCell"></td>', # A/D
|
||||
'<td class="TableCell"></td>', # Price
|
||||
f'<td class="TableCell Right">{shares_owned}</td>',
|
||||
f'<td class="TableCell Centered">{direct_indirect_code}</td>',
|
||||
f'<td class="TableCell">{nature_of_ownership}</td>'
|
||||
]
|
||||
non_deriv_rows.append(row)
|
||||
|
||||
# Add non_deriv_rows to context
|
||||
context['non_deriv_rows'] = non_deriv_rows
|
||||
|
||||
# Process Table II - Derivative Securities
|
||||
deriv_rows = []
|
||||
derivative_table = getattr(ownership, 'derivative_table', None)
|
||||
if derivative_table:
|
||||
# Form 3 - Initial derivative holdings
|
||||
if form_type == '3' and hasattr(derivative_table, 'holdings') and derivative_table.holdings is not None and not derivative_table.holdings.empty:
|
||||
for holding_tuple in derivative_table.holdings.data.itertuples(index=False):
|
||||
# Convert tuple to dictionary for easier access with default values
|
||||
holding = {col: getattr(holding_tuple, col, '') for col in holding_tuple._fields}
|
||||
|
||||
# Extract values using dictionary get() with defaults
|
||||
security_title = str(holding.get('Security', ''))
|
||||
conversion_price = format_price(holding.get('ExercisePrice', ''))
|
||||
exercisable_date = _format_date(holding.get('ExerciseDate', ''))
|
||||
expiration_date = _format_date(holding.get('ExpirationDate', ''))
|
||||
exercisable_expiration = f"{exercisable_date} - {expiration_date}"
|
||||
underlying_title = str(holding.get('Underlying', ''))
|
||||
underlying_shares = format_numeric(holding.get('UnderlyingShares', ''))
|
||||
title_amount_underlying = f"{underlying_title} - {underlying_shares}"
|
||||
direct_indirect_code = holding.get('DirectIndirect', '')
|
||||
nature_of_ownership = str(holding.get('Nature Of Ownership', ''))
|
||||
|
||||
# Clean footnotes from values for table display
|
||||
security_title_clean = re.sub(r'<[Ss][Uu][Pp]>.*?</[Ss][Uu][Pp]>', '', security_title).strip()
|
||||
exercisable_expiration_clean = re.sub(r'<[Ss][Uu][Pp]>.*?</[Ss][Uu][Pp]>', '', exercisable_expiration).strip()
|
||||
title_amount_underlying_clean = re.sub(r'<[Ss][Uu][Pp]>.*?</[Ss][Uu][Pp]>', '', title_amount_underlying).strip()
|
||||
|
||||
row = [
|
||||
f'<td class="TableCell">{security_title_clean}</td>',
|
||||
f'<td class="TableCell Right">{conversion_price}</td>',
|
||||
'<td class="TableCell">N/A</td>', # Transaction Date (blank for F3 initial holding)
|
||||
'<td class="TableCell">N/A</td>', # Deemed Execution Date (blank for F3 initial holding)
|
||||
'<td class="TableCell">N/A</td>', # Transaction Code (blank for F3 initial holding)
|
||||
'<td class="TableCell">N/A</td>', # V (blank for F3 initial holding)
|
||||
'<td class="TableCell">N/A</td>', # Shares in transaction (blank for F3 initial holding)
|
||||
'<td class="TableCell">N/A</td>', # A/D (blank for F3 initial holding)
|
||||
f'<td class="TableCell">{exercisable_expiration_clean}</td>',
|
||||
f'<td class="TableCell">{title_amount_underlying_clean}</td>',
|
||||
'<td class="TableCell">N/A</td>', # Price (blank for F3 initial holding)
|
||||
'<td class="TableCell">N/A</td>', # Amount owned after transaction (blank for F3 initial holding)
|
||||
f'<td class="TableCell Centered">{direct_indirect_code}</td>',
|
||||
f'<td class="TableCell">{nature_of_ownership}</td>'
|
||||
]
|
||||
deriv_rows.append(row)
|
||||
|
||||
# Form 4/5 - Derivative Transactions
|
||||
elif form_type in ['4', '5'] and hasattr(derivative_table, 'transactions') and derivative_table.transactions is not None and not derivative_table.transactions.empty:
|
||||
for transaction_tuple in derivative_table.transactions.data.itertuples(index=False):
|
||||
# Convert tuple to dictionary for easier access with default values
|
||||
transaction = {col: getattr(transaction_tuple, col, '') for col in transaction_tuple._fields}
|
||||
|
||||
# Extract values using dictionary get() with defaults
|
||||
security_title = str(transaction.get('Security', ''))
|
||||
conversion_price = format_price(transaction.get('ExercisePrice', ''))
|
||||
transaction_date = _format_date(transaction.get('Date', ''))
|
||||
deemed_date = _format_date(transaction.get('DeemedDate', ''))
|
||||
transaction_code = str(transaction.get('Code', ''))
|
||||
transaction_v = str(transaction.get('V', ''))
|
||||
shares = format_numeric(str(transaction.get('Shares', '')))
|
||||
acquired_disposed = str(transaction.get('AcquiredDisposed', ''))
|
||||
exercisable_date = _format_date(transaction.get('ExerciseDate', ''))
|
||||
expiration_date = _format_date(transaction.get('ExpirationDate', ''))
|
||||
exercisable_expiration = f"{exercisable_date} - {expiration_date}"
|
||||
underlying_title = str(transaction.get('Underlying', ''))
|
||||
underlying_shares = format_numeric(transaction.get('UnderlyingShares', ''))
|
||||
title_amount_underlying = f"{underlying_title} - {underlying_shares}"
|
||||
price = format_price(transaction.get('Price', ''))
|
||||
owned_after_transaction = format_numeric(str(transaction.get('Remaining', '')))
|
||||
direct_indirect_code = str(transaction.get('DirectIndirect', ''))
|
||||
nature_of_ownership = str(transaction.get('NatureOfOwnership', ''))
|
||||
|
||||
# Clean footnotes from values for table display
|
||||
security_title_clean = re.sub(r'<[Ss][Uu][Pp]>.*?</[Ss][Uu][Pp]>', '', security_title).strip()
|
||||
exercisable_expiration_clean = re.sub(r'<[Ss][Uu][Pp]>.*?</[Ss][Uu][Pp]>', '', exercisable_expiration).strip()
|
||||
title_amount_underlying_clean = re.sub(r'<[Ss][Uu][Pp]>.*?</[Ss][Uu][Pp]>', '', title_amount_underlying).strip()
|
||||
|
||||
row = [
|
||||
f'<td class="TableCell">{security_title_clean}</td>',
|
||||
f'<td class="TableCell Right">{conversion_price}</td>',
|
||||
f'<td class="TableCell">{transaction_date}</td>',
|
||||
f'<td class="TableCell">{deemed_date}</td>',
|
||||
f'<td class="TableCell">{transaction_code}</td>',
|
||||
f'<td class="TableCell">{transaction_v}</td>',
|
||||
f'<td class="TableCell Right">{shares}</td>',
|
||||
f'<td class="TableCell Centered">{acquired_disposed}</td>',
|
||||
f'<td class="TableCell">{exercisable_expiration_clean}</td>',
|
||||
f'<td class="TableCell">{title_amount_underlying_clean}</td>',
|
||||
f'<td class="TableCell Right">{price}</td>',
|
||||
f'<td class="TableCell Right">{owned_after_transaction}</td>',
|
||||
f'<td class="TableCell Centered">{direct_indirect_code}</td>',
|
||||
f'<td class="TableCell">{nature_of_ownership}</td>'
|
||||
]
|
||||
deriv_rows.append(row)
|
||||
|
||||
# Process Holdings for Forms 4/5
|
||||
if hasattr(derivative_table, 'holdings') and derivative_table.holdings:
|
||||
for holding_tuple in derivative_table.holdings.data.itertuples(index=False):
|
||||
# Convert tuple to dictionary for easier access with default values
|
||||
holding = {col: getattr(holding_tuple, col, '') for col in holding_tuple._fields}
|
||||
|
||||
# Extract values using dictionary get() with defaults
|
||||
security_title = str(holding.get('Security', ''))
|
||||
conversion_price = format_price(holding.get('ExercisePrice', ''))
|
||||
exercisable_date = _format_date(holding.get('ExerciseDate', ''))
|
||||
expiration_date = _format_date(holding.get('ExpirationDate', ''))
|
||||
exercisable_expiration = f"{exercisable_date} - {expiration_date}"
|
||||
underlying_title = str(holding.get('Underlying', ''))
|
||||
underlying_shares = format_numeric(holding.get('UnderlyingShares', ''))
|
||||
title_amount_underlying = f"{underlying_title} - {underlying_shares}"
|
||||
owned_after_transaction = format_numeric(holding.get('Remaining', ''))
|
||||
direct_indirect_code = holding.get('DirectIndirect', '')
|
||||
nature_of_ownership = str(holding.get('NatureOfOwnership', ''))
|
||||
|
||||
# Clean footnotes from values for table display
|
||||
security_title_clean = re.sub(r'<[Ss][Uu][Pp]>.*?</[Ss][Uu][Pp]>', '', security_title).strip()
|
||||
exercisable_expiration_clean = re.sub(r'<[Ss][Uu][Pp]>.*?</[Ss][Uu][Pp]>', '', exercisable_expiration).strip()
|
||||
title_amount_underlying_clean = re.sub(r'<[Ss][Uu][Pp]>.*?</[Ss][Uu][Pp]>', '', title_amount_underlying).strip()
|
||||
|
||||
row = [
|
||||
f'<td class="TableCell">{security_title_clean}</td>',
|
||||
f'<td class="TableCell Right">{conversion_price}</td>',
|
||||
'<td class="TableCell"></td>', # Transaction Date
|
||||
'<td class="TableCell"></td>', # Deemed Execution Date
|
||||
'<td class="TableCell"></td>', # Transaction Code
|
||||
'<td class="TableCell"></td>', # V
|
||||
'<td class="TableCell"></td>', # Shares in transaction
|
||||
'<td class="TableCell"></td>', # A/D
|
||||
f'<td class="TableCell">{exercisable_expiration_clean}</td>',
|
||||
f'<td class="TableCell">{title_amount_underlying_clean}</td>',
|
||||
'<td class="TableCell"></td>', # Price
|
||||
f'<td class="TableCell Right">{owned_after_transaction}</td>',
|
||||
f'<td class="TableCell Centered">{direct_indirect_code}</td>',
|
||||
f'<td class="TableCell">{nature_of_ownership}</td>'
|
||||
]
|
||||
deriv_rows.append(row)
|
||||
# Add deriv_rows to context
|
||||
context['deriv_rows'] = deriv_rows
|
||||
|
||||
# Render the template with the context
|
||||
return template.render(**context)
|
||||
|
||||
def _parse_name(name: str):
|
||||
"""Parse a full name into (last, first, middle) components"""
|
||||
if not name:
|
||||
return '', '', ''
|
||||
|
||||
# Check for comma format: "Last, First Middle"
|
||||
if ',' in name:
|
||||
last, rest = name.split(',', 1)
|
||||
parts = rest.strip().split()
|
||||
first = parts[0] if parts else ''
|
||||
middle = ' '.join(parts[1:]) if len(parts) > 1 else ''
|
||||
else:
|
||||
# Assume format "First Middle Last"
|
||||
parts = name.split()
|
||||
last = parts[-1] if parts else ''
|
||||
first = parts[0] if parts else ''
|
||||
middle = ' '.join(parts[1:-1]) if len(parts) > 1 else ''
|
||||
|
||||
return last.strip(), first.strip(), middle.strip()
|
||||
2006
venv/lib/python3.10/site-packages/edgar/ownership/ownershipforms.py
Normal file
2006
venv/lib/python3.10/site-packages/edgar/ownership/ownershipforms.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,35 @@
|
||||
<!doctype html public "-//w3c//dtd xhtml">
|
||||
<html>
|
||||
<head>
|
||||
<title>{{ html_title }}</title>
|
||||
<style>
|
||||
body {font-family: Arial, sans-serif; margin: 20px; font-size: 9pt; color: #000;}
|
||||
table {border-collapse: collapse; width: 100%;}
|
||||
th, td {border: 1px solid #777; padding: 3px; text-align: left; vertical-align: top; font-size:8pt;}
|
||||
th {background-color: #f0f0f0; font-weight: bold; text-align:center;}
|
||||
.TableCell {font-size:8pt;}
|
||||
.Centered {text-align: center;}
|
||||
.Right {text-align: right;}
|
||||
.Bold {font-weight: bold;}
|
||||
.SmallText {font-size: 7pt;}
|
||||
.FormTitle {font-size: 12pt; font-weight: bold; text-align: center; margin-bottom: 5px;}
|
||||
.Header {font-size: 10pt; font-weight: bold; text-align: center; margin-bottom: 5px;}
|
||||
.SubHeader {font-size: 9pt; text-align: center; margin-bottom: 15px;}
|
||||
.SectionHeader {font-size: 10pt; font-weight: bold; margin-top: 10px; margin-bottom: 5px;}
|
||||
.IssuerInfo, .ReportingOwnerInfo {border: 1px solid #000; padding: 5px; margin-bottom:10px; width: 48%; display:inline-block; vertical-align:top;}
|
||||
.InfoBoxTable td {border:none; font-size:8pt; padding:1px;}
|
||||
.Footnotes {margin-top: 20px; font-size: 8pt;}
|
||||
.FootnoteList {list-style-type: none; padding-left: 0;}
|
||||
.FootnoteList li {margin-bottom: 3px;}
|
||||
.SignatureBlock {margin-top:30px; font-size:9pt;}
|
||||
.SignatureBlock .SignatureDate {float:right;}
|
||||
.Remarks {margin-top:15px; font-size:9pt;}
|
||||
.RemarkText {margin-left:10px; font-size:8pt;}
|
||||
.Checkbox {display: inline-block; width: 12px; height: 12px; border: 1px solid #000; text-align: center; line-height: 12px;}
|
||||
.NoBorder, .NoBorder td, .NoBorder th {border: none;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
{% block content %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,234 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<!-- OMB Header -->
|
||||
<div class="SmallText Right">
|
||||
OMB APPROVAL<br>
|
||||
OMB Number: 3235-0287<br>
|
||||
Estimated average burden<br>
|
||||
hours per response: 0.5
|
||||
</div>
|
||||
|
||||
<div class="FormTitle">{{ form_name_display }}</div>
|
||||
<div class="SubHeader">Check this box if no longer subject to Section 16. Form 4 or Form 5 obligations may continue. See Instruction 1(b). <span class="Checkbox"></span></div>
|
||||
|
||||
<div class="Header">UNITED STATES SECURITIES AND EXCHANGE COMMISSION<br>Washington, D.C. 20549</div>
|
||||
<div class="Header">{{ form_title_display }}</div>
|
||||
<div class="SmallText Centered" style="margin-bottom:10px;">Filed pursuant to Section 16(a) of the Securities Exchange Act of 1934<br>or Section 30(h) of the Investment Company Act of 1940</div>
|
||||
|
||||
<table class="NoBorder" style="margin-bottom:10px;">
|
||||
<tr>
|
||||
<td style="width:60%; vertical-align:top;">
|
||||
<div><span class="Bold">1. Name and Address of Reporting Person*</span></div>
|
||||
<div style="margin-left:10px; margin-top:2px;">
|
||||
{{ reporting_owner_name_str }}<br>
|
||||
{{ reporting_owner_address_str|safe }}
|
||||
</div>
|
||||
|
||||
<div style="margin-top:5px;"><span class="Bold">1a. IRS/SSN Identification Number of Reporting Person</span></div>
|
||||
<div style="margin-left:10px; margin-top:2px;"> </div>
|
||||
</td>
|
||||
<td style="width:40%; vertical-align:top;">
|
||||
<div><span class="Bold">2. Issuer Name and Ticker or Trading Symbol</span></div>
|
||||
<div style="margin-left:10px; margin-top:2px;">{{ issuer_name }} [{{ ticker }}]</div>
|
||||
|
||||
<div style="margin-top:5px;"><span class="Bold">3. Date of Earliest Transaction (Month/Day/Year)</span></div>
|
||||
<div style="margin-left:10px; margin-top:2px;">{{ reporting_period }}</div>
|
||||
|
||||
<div style="margin-top:5px;"><span class="Bold">4. If Amendment, Date of Original Filed (Month/Day/Year)</span></div>
|
||||
<div style="margin-left:10px; margin-top:2px;"> </div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" style="padding-top:5px;">
|
||||
<span class="Bold">5. Relationship of Reporting Person(s) to Issuer</span><br>
|
||||
<span class="SmallText">(Check all applicable)</span>
|
||||
<div style="margin-left:10px; margin-top:2px;">
|
||||
<span class="Checkbox">{% if is_director %}X{% else %} {% endif %}</span> Director
|
||||
<span class="Checkbox">{% if is_officer %}X{% else %} {% endif %}</span> Officer (give title below)
|
||||
<span class="Checkbox">{% if is_ten_pct_owner %}X{% else %} {% endif %}</span> 10% Owner <br>
|
||||
<span class="Checkbox">{% if is_other %}X{% else %} {% endif %}</span> Other (specify below)
|
||||
<div style="margin-left:25px; margin-top:2px; font-size:7pt;">{{ officer_title }}</div>
|
||||
<div style="margin-left:25px; margin-top:2px; font-size:7pt;">{{ relationship_str }}</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" style="padding-top:5px;">
|
||||
<span class="Bold">6. Individual or Joint/Group Filing (Check Applicable Line)</span>
|
||||
<div style="margin-left:10px; margin-top:2px;">
|
||||
<span class="Checkbox">{% if is_individual_filing %}X{% else %} {% endif %}</span> Form filed by One Reporting Person<br>
|
||||
<span class="Checkbox">{% if is_joint_filing %}X{% else %} {% endif %}</span> Form filed by More than One Reporting Person
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Table I - Non-Derivative Securities -->
|
||||
<h4 class="SectionHeader" style="text-align:center;">
|
||||
Table I - Non-Derivative Securities {% if form_type == '3' %}Beneficially Owned{% else %}Acquired, Disposed of, or Beneficially Owned{% endif %}
|
||||
</h4>
|
||||
|
||||
<table class="ReportTable">
|
||||
<thead>
|
||||
{% if form_type == '3' %}
|
||||
<!-- Form 3 Table I Header -->
|
||||
<tr>
|
||||
<th>1. Title of Security (Instr. 3)</th>
|
||||
<th>2. Amount of Securities Beneficially Owned (Instr. 4)</th>
|
||||
<th>3. Ownership Form: Direct (D) or Indirect (I) (Instr. 4)</th>
|
||||
<th>4. Nature of Indirect Beneficial Ownership (Instr. 4)</th>
|
||||
</tr>
|
||||
{% else %}
|
||||
<!-- Forms 4 & 5 Table I Header -->
|
||||
<tr>
|
||||
<th rowspan="2">1. Title of Security (Instr. 3)</th>
|
||||
<th rowspan="2">2. Transaction Date (Month/Day/Year)</th>
|
||||
<th rowspan="2">2A. Deemed Execution Date, if any (Month/Day/Year)</th>
|
||||
<th colspan="2">3. Transaction Code (Instr. 8)</th>
|
||||
<th colspan="3">4. Securities Acquired (A) or Disposed Of (D) (Instr. 3, 4 and 5)</th>
|
||||
<th rowspan="2">5. Amount of Securities Beneficially<br/>Owned Following Reported Transaction(s)<br/>(Instr. 3 and 4)</th>
|
||||
<th rowspan="2">6. Ownership Form: Direct (D) or Indirect (I) <br/>(Instr. 4)</th>
|
||||
<th rowspan="2">7. Nature of Indirect Beneficial Ownership <br/>(Instr. 4)</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Code</th>
|
||||
<th>V</th>
|
||||
<th>Amount</th>
|
||||
<th>(A) or (D)</th>
|
||||
<th>Price</th>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</thead>
|
||||
<tbody>
|
||||
{% if non_deriv_rows %}
|
||||
{% for row in non_deriv_rows %}
|
||||
<tr>
|
||||
{% for cell in row %}
|
||||
{{ cell|safe }}
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="{% if form_type == '3' %}4{% else %}11{% endif %}" class="Centered">No non-derivative securities reported</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Table II - Derivative Securities -->
|
||||
<h4 class="SectionHeader" style="text-align:center; margin-top:20px;">
|
||||
{% if form_type == '3' %}
|
||||
Table II - Derivative Securities Beneficially Owned
|
||||
{% else %}
|
||||
Table II - Derivative Securities Acquired, Disposed of, or Beneficially Owned
|
||||
{% endif %}
|
||||
<br>
|
||||
<span style="font-weight:normal; font-size:8pt;">(e.g., puts, calls, warrants, options, convertible securities)</span>
|
||||
</h4>
|
||||
|
||||
<table class="ReportTable">
|
||||
<thead>
|
||||
{% if form_type == '3' %}
|
||||
<!-- Form 3 Table II Header -->
|
||||
<tr>
|
||||
<th>1. Title of Derivative Security (Instr. 5)</th>
|
||||
<th>2. Date Exercisable and Expiration Date (Month/Day/Year)</th>
|
||||
<th>3. Title and Amount of Securities Underlying Derivative Security (Instr. 4)</th>
|
||||
<th>4. Conversion or Exercise Price of Derivative Security</th>
|
||||
<th>5. Ownership Form: Direct (D) or Indirect (I) (Instr. 5)</th>
|
||||
<th>6. Nature of Indirect Beneficial Ownership (Instr. 5)</th>
|
||||
</tr>
|
||||
{% else %}
|
||||
<!-- Forms 4 & 5 Table II Header -->
|
||||
<tr>
|
||||
<th rowspan="2">1. Title of Derivative Security (Instr. 5)</th>
|
||||
<th rowspan="2">2. Conversion or Exercise Price of Derivative Security</th>
|
||||
<th rowspan="2">3. Transaction Date (Month/Day/Year)</th>
|
||||
<th rowspan="2">3A. Deemed Execution Date, if any (Month/Day/Year)</th>
|
||||
<th colspan="2">4. Transaction Code (Instr. 8)</th>
|
||||
<th colspan="2">5. Number of Derivative Securities Acquired (A) or Disposed of (D) (Instr. 3, 4 and 5)</th>
|
||||
<th rowspan="2">6. Date Exercisable and Expiration Date (Month/Day/Year)</th>
|
||||
<th rowspan="2">7. Title and Amount of Securities Underlying Derivative Security (Instr. 3 and 4)</th>
|
||||
<th rowspan="2">8. Price of Derivative Security</th>
|
||||
<th rowspan="2">9. Number of derivative Securities Beneficially Owned Following Reported Transaction(s) (Instr. 4)</th>
|
||||
<th rowspan="2">10. Ownership Form of Derivative Security: Direct (D) or Indirect (I) (Instr. 4)</th>
|
||||
<th rowspan="2">11. Nature of Indirect Beneficial Ownership (Instr. 5)</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Code</th>
|
||||
<th>V</th>
|
||||
<th>Amount or Number of Shares</th>
|
||||
<th>(A) or (D)</th>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</thead>
|
||||
<tbody>
|
||||
{% if deriv_rows %}
|
||||
{% for row in deriv_rows %}
|
||||
<tr>
|
||||
{% for cell in row %}
|
||||
{{ cell|safe }}
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="{% if form_type == '3' %}6{% else %}16{% endif %}" class="Centered">No derivative securities reported</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Remarks Section -->
|
||||
{% if remarks %}
|
||||
<div class="Remarks">
|
||||
<span class="Bold">Remarks:</span><br>
|
||||
<div class="RemarkText">{{ remarks }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Footnotes Section -->
|
||||
{% if footnotes %}
|
||||
<div class="Footnotes">
|
||||
<h4 class="SectionHeader">Explanation of Responses:</h4>
|
||||
<div class="FootnoteList">
|
||||
{% for footnote in footnotes %}
|
||||
<li><span class="Bold">{{ footnote.id }}:</span> {{ footnote.text }}</li>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Signature Section -->
|
||||
<div class="SignatureBlock">
|
||||
<table style="width:auto; border:none;">
|
||||
<tr>
|
||||
{% if sig_name %}
|
||||
<td style="padding-right:30px;">
|
||||
<div class="SmallText">** Signature of Reporting Person</div>
|
||||
<div>/s/ {{ sig_name }}</div>
|
||||
</td>
|
||||
{% else %}
|
||||
<td></td>
|
||||
{% endif %}
|
||||
|
||||
{% if sig_date %}
|
||||
<td>
|
||||
<div class="SmallText">Date</div>
|
||||
<div>{{ sig_date }}</div>
|
||||
</td>
|
||||
{% else %}
|
||||
<td></td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Disclaimer and OMB Notice -->
|
||||
<div style="margin-top:10px; font-size:7pt; font-style:italic;">
|
||||
Note: File three copies of this Form, one of which must be manually signed. If space is insufficient, see Instruction 6 for procedure.<br>
|
||||
Persons who respond to the collection of information contained in this form are not required to respond unless the form displays a currently valid OMB Number.
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user