""" Current Period API - Convenient access to current period financial data. This module provides the CurrentPeriodView class that offers simplified access to the most recent period's financial data without comparative information, addressing GitHub issue #425. Key features: - Automatic detection of the current (most recent) period - Direct access to balance sheet, income statement, and cash flow data - Support for raw XBRL concept names (unprocessed) - Notes and disclosures access - Beginner-friendly API design """ from datetime import date, datetime from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union import pandas as pd from edgar.core import log from edgar.richtools import repr_rich from edgar.xbrl.exceptions import StatementNotFound if TYPE_CHECKING: from edgar.xbrl.statements import Statement class CurrentPeriodView: """ Convenient access to current period financial data. This class provides simplified access to the most recent period's financial data without comparative information. It automatically detects the current period and provides easy access to key statements. Example usage: >>> xbrl = filing.xbrl() >>> current = xbrl.current_period >>> balance_sheet = current.balance_sheet() >>> income_statement = current.income_statement(raw_concepts=True) """ def __init__(self, xbrl): """ Initialize CurrentPeriodView with an XBRL object. Args: xbrl: XBRL object containing parsed financial data """ self.xbrl = xbrl self._current_period_key = None self._current_period_label = None @property def period_key(self) -> str: """ Get the current period key (most recent period). The current period is determined by: 1. Document period end date if available 2. Most recent period in reporting periods 3. Fallback to any available period Returns: Period key string (e.g., "instant_2024-12-31" or "duration_2024-01-01_2024-12-31") """ if self._current_period_key is None: self._current_period_key = self._detect_current_period() return self._current_period_key @property def period_label(self) -> str: """ Get the human-readable label for the current period. Returns: Human-readable period label (e.g., "December 31, 2024" or "Year Ended December 31, 2024") """ if self._current_period_label is None: self._detect_current_period() # This sets both key and label return self._current_period_label or self.period_key def _detect_current_period(self) -> str: """ Detect the current (most recent) period from available data. Strategy: 1. Use document period end date to find matching instant period 2. If no instant match, find most recent duration period ending on document period end 3. Fall back to most recent period by end date 4. Final fallback to first available period Returns: Period key for the current period """ if not self.xbrl.reporting_periods: log.warning("No reporting periods found in XBRL data") return "" # Try to use document period end date if available document_period_end = None if hasattr(self.xbrl, 'period_of_report') and self.xbrl.period_of_report: try: if isinstance(self.xbrl.period_of_report, str): document_period_end = datetime.strptime(self.xbrl.period_of_report, '%Y-%m-%d').date() elif isinstance(self.xbrl.period_of_report, (date, datetime)): document_period_end = self.xbrl.period_of_report if isinstance(document_period_end, datetime): document_period_end = document_period_end.date() except (ValueError, TypeError): log.debug(f"Could not parse document period end date: {self.xbrl.period_of_report}") # Sort periods by end date (most recent first) periods_by_date = [] for period in self.xbrl.reporting_periods: period_key = period['key'] period_label = period.get('label', period_key) end_date = None try: if period_key.startswith('instant_'): # Format: "instant_2024-12-31" date_str = period_key.split('_', 1)[1] end_date = datetime.strptime(date_str, '%Y-%m-%d').date() elif period_key.startswith('duration_'): # Format: "duration_2024-01-01_2024-12-31" parts = period_key.split('_') if len(parts) >= 3: date_str = parts[2] # End date end_date = datetime.strptime(date_str, '%Y-%m-%d').date() if end_date: periods_by_date.append((end_date, period_key, period_label)) except (ValueError, IndexError): log.debug(f"Could not parse period key: {period_key}") continue if not periods_by_date: # Fallback to first available period if no dates could be parsed first_period = self.xbrl.reporting_periods[0] self._current_period_key = first_period['key'] self._current_period_label = first_period.get('label', first_period['key']) log.debug(f"Using fallback period: {self._current_period_key}") return self._current_period_key # Sort by date (most recent first) periods_by_date.sort(key=lambda x: x[0], reverse=True) # Strategy 1: If we have document period end, look for exact matches # Prefer instant periods over duration periods when both match document end date if document_period_end: instant_match = None duration_match = None for end_date, period_key, period_label in periods_by_date: if end_date == document_period_end: if period_key.startswith('instant_'): instant_match = (period_key, period_label) elif period_key.startswith('duration_'): duration_match = (period_key, period_label) # Prefer instant match if available if instant_match: self._current_period_key = instant_match[0] self._current_period_label = instant_match[1] log.debug(f"Found instant period matching document end date: {instant_match[0]}") return self._current_period_key elif duration_match: self._current_period_key = duration_match[0] self._current_period_label = duration_match[1] log.debug(f"Found duration period matching document end date: {duration_match[0]}") return self._current_period_key # Strategy 2: Use most recent period most_recent = periods_by_date[0] self._current_period_key = most_recent[1] self._current_period_label = most_recent[2] log.debug(f"Selected most recent period: {self._current_period_key} ({self._current_period_label})") return self._current_period_key def _get_appropriate_period_for_statement(self, statement_type: str) -> str: """ Get the appropriate period type for the given statement type. Balance sheet items are point-in-time (instant periods). Income statement and cash flow items represent activities over time (duration periods). Args: statement_type: Type of statement ('BalanceSheet', 'IncomeStatement', etc.) Returns: Period key appropriate for the statement type """ # Statements that use instant periods (point in time) instant_statements = { 'BalanceSheet', 'StatementOfEquity', 'StatementOfFinancialPosition' } # Statements that use duration periods (period of time) duration_statements = { 'IncomeStatement', 'CashFlowStatement', 'ComprehensiveIncome', 'StatementOfOperations', 'StatementOfCashFlows' } if statement_type in instant_statements: # Use the current instant period return self.period_key elif statement_type in duration_statements: # Find the most recent duration period with the same end date if not self.xbrl.reporting_periods: return self.period_key # Fallback to current period # Get the end date from the current period (which might be instant) current_end_date = None current_period_key = self.period_key if current_period_key.startswith('instant_'): # Extract date from instant period date_str = current_period_key.split('_', 1)[1] try: from datetime import datetime current_end_date = datetime.strptime(date_str, '%Y-%m-%d').date() except (ValueError, IndexError): return self.period_key # Fallback elif current_period_key.startswith('duration_'): # Extract end date from duration period parts = current_period_key.split('_') if len(parts) >= 3: try: from datetime import datetime current_end_date = datetime.strptime(parts[2], '%Y-%m-%d').date() except (ValueError, IndexError): return self.period_key # Fallback if current_end_date: # Look for a duration period ending on the same date # Prefer annual periods, then quarterly, then other durations matching_periods = [] for period in self.xbrl.reporting_periods: period_key = period['key'] if period_key.startswith('duration_'): parts = period_key.split('_') if len(parts) >= 3: try: from datetime import datetime end_date = datetime.strptime(parts[2], '%Y-%m-%d').date() if end_date == current_end_date: period_type = period.get('period_type', '') priority = 1 if period_type == 'Annual' else (2 if period_type == 'Quarterly' else 3) matching_periods.append((priority, period_key, period.get('label', period_key))) except (ValueError, IndexError): continue if matching_periods: # Sort by priority (1=Annual, 2=Quarterly, 3=Other) and return the best match matching_periods.sort(key=lambda x: x[0]) selected_period = matching_periods[0][1] log.debug(f"Selected duration period for {statement_type}: {selected_period}") return selected_period # Fallback: use current period even if it's not ideal return self.period_key else: # Unknown statement type, use current period log.debug(f"Unknown statement type {statement_type}, using current period: {self.period_key}") return self.period_key def balance_sheet(self, raw_concepts: bool = False, as_statement: bool = True) -> Union[pd.DataFrame, 'Statement']: """ Get current period balance sheet data. Args: raw_concepts: If True, preserve original XBRL concept names (e.g., "us-gaap:Assets" instead of "Assets") as_statement: If True, return a Statement object (default), if False, return DataFrame Returns: Statement object with rich formatting by default, or pandas DataFrame if as_statement=False Example: >>> stmt = xbrl.current_period.balance_sheet() >>> print(stmt) # Rich formatted table >>> df = xbrl.current_period.balance_sheet(as_statement=False) >>> assets = df[df['label'].str.contains('Assets', case=False)]['value'].iloc[0] """ if as_statement: return self._get_statement_object('BalanceSheet') return self._get_statement_dataframe('BalanceSheet', raw_concepts=raw_concepts) def income_statement(self, raw_concepts: bool = False, as_statement: bool = True) -> Union[pd.DataFrame, 'Statement']: """ Get current period income statement data. Args: raw_concepts: If True, preserve original XBRL concept names (e.g., "us-gaap:Revenues" instead of "Revenue") as_statement: If True, return a Statement object (default), if False, return DataFrame Returns: Statement object with rich formatting by default, or pandas DataFrame if as_statement=False Example: >>> stmt = xbrl.current_period.income_statement() >>> print(stmt) # Rich formatted table >>> df = xbrl.current_period.income_statement(as_statement=False, raw_concepts=True) >>> revenue = df[df['concept'].str.contains('Revenues')]['value'].iloc[0] """ if as_statement: return self._get_statement_object('IncomeStatement') return self._get_statement_dataframe('IncomeStatement', raw_concepts=raw_concepts) def cashflow_statement(self, raw_concepts: bool = False, as_statement: bool = True) -> Union[pd.DataFrame, 'Statement']: """ Get current period cash flow statement data. Args: raw_concepts: If True, preserve original XBRL concept names (e.g., "us-gaap:NetCashProvidedByUsedInOperatingActivities") as_statement: If True, return a Statement object (default), if False, return DataFrame Returns: Statement object with rich formatting by default, or pandas DataFrame if as_statement=False Example: >>> stmt = xbrl.current_period.cashflow_statement() >>> print(stmt) # Rich formatted table >>> df = xbrl.current_period.cashflow_statement(as_statement=False) >>> operating_cf = df[df['label'].str.contains('Operating')]['value'].iloc[0] """ if as_statement: return self._get_statement_object('CashFlowStatement') return self._get_statement_dataframe('CashFlowStatement', raw_concepts=raw_concepts) def statement_of_equity(self, raw_concepts: bool = False, as_statement: bool = True) -> Union[pd.DataFrame, 'Statement']: """ Get current period statement of equity data. Args: raw_concepts: If True, preserve original XBRL concept names as_statement: If True, return a Statement object (default), if False, return DataFrame Returns: Statement object with rich formatting by default, or pandas DataFrame if as_statement=False """ if as_statement: return self._get_statement_object('StatementOfEquity') return self._get_statement_dataframe('StatementOfEquity', raw_concepts=raw_concepts) def comprehensive_income(self, raw_concepts: bool = False, as_statement: bool = True) -> Union[pd.DataFrame, 'Statement']: """ Get current period comprehensive income statement data. Args: raw_concepts: If True, preserve original XBRL concept names as_statement: If True, return a Statement object (default), if False, return DataFrame Returns: Statement object with rich formatting by default, or pandas DataFrame if as_statement=False """ if as_statement: return self._get_statement_object('ComprehensiveIncome') return self._get_statement_dataframe('ComprehensiveIncome', raw_concepts=raw_concepts) def _get_statement_dataframe(self, statement_type: str, raw_concepts: bool = False) -> pd.DataFrame: """ Internal method to get statement data as DataFrame for current period. Args: statement_type: Type of statement ('BalanceSheet', 'IncomeStatement', etc.) raw_concepts: Whether to preserve raw XBRL concept names Returns: pandas DataFrame with statement data filtered to current period Raises: StatementNotFound: If the requested statement type is not available """ try: # Select appropriate period based on statement type period_filter = self._get_appropriate_period_for_statement(statement_type) # Get raw statement data filtered to current period statement_data = self.xbrl.get_statement(statement_type, period_filter=period_filter) if not statement_data: entity_name = getattr(self.xbrl, 'entity_name', 'Unknown') raise StatementNotFound( statement_type=statement_type, confidence=0.0, found_statements=[], entity_name=entity_name, reason=f"No data found for {statement_type} in period {self.period_label}" ) # Convert to DataFrame rows = [] for item in statement_data: # Get the value for appropriate period values = item.get('values', {}) current_value = values.get(period_filter) if current_value is not None: row = { 'concept': self._get_concept_name(item, raw_concepts), 'label': item.get('label', ''), 'value': current_value, 'level': item.get('level', 0), 'is_abstract': item.get('is_abstract', False) } # Add original concept name if raw_concepts is requested if raw_concepts: row['standardized_label'] = item.get('label', '') # Try to get original concept names from all_names all_names = item.get('all_names', []) if all_names: row['original_concept'] = all_names[0] # First is usually original # Add dimension information if present if item.get('is_dimension', False): row['dimension_label'] = item.get('full_dimension_label', '') row['is_dimension'] = True rows.append(row) if not rows: # Create empty DataFrame with expected structure columns = ['concept', 'label', 'value', 'level', 'is_abstract'] if raw_concepts: columns.extend(['standardized_label', 'original_concept']) return pd.DataFrame(columns=columns) return pd.DataFrame(rows) except Exception as e: log.error(f"Error retrieving {statement_type} for current period: {str(e)}") entity_name = getattr(self.xbrl, 'entity_name', 'Unknown') raise StatementNotFound( statement_type=statement_type, confidence=0.0, found_statements=[], entity_name=entity_name, reason=f"Failed to retrieve {statement_type}: {str(e)}" ) from e def _get_statement_object(self, statement_type: str) -> 'Statement': """ Internal method to get statement as a Statement object for current period. Args: statement_type: Type of statement ('BalanceSheet', 'IncomeStatement', etc.) Returns: Statement object with current period filtering applied Raises: StatementNotFound: If the requested statement type is not available """ try: # Import here to avoid circular imports # Select appropriate period based on statement type period_filter = self._get_appropriate_period_for_statement(statement_type) # Find the statement using the unified statement finder matching_statements, found_role, actual_statement_type = self.xbrl.find_statement(statement_type) if not found_role: entity_name = getattr(self.xbrl, 'entity_name', 'Unknown') raise StatementNotFound( statement_type=statement_type, confidence=0.0, found_statements=[], entity_name=entity_name, reason=f"No matching {statement_type} found for current period {self.period_label}" ) # Create a Statement object with period filtering # We'll create a custom Statement class that applies period filtering statement = CurrentPeriodStatement( self.xbrl, found_role, canonical_type=statement_type, period_filter=period_filter, period_label=self.period_label ) return statement except Exception as e: log.error(f"Error retrieving {statement_type} statement object for current period: {str(e)}") entity_name = getattr(self.xbrl, 'entity_name', 'Unknown') raise StatementNotFound( statement_type=statement_type, confidence=0.0, found_statements=[], entity_name=entity_name, reason=f"Failed to retrieve {statement_type} statement: {str(e)}" ) from e def _get_concept_name(self, item: Dict[str, Any], raw_concepts: bool) -> str: """ Get the appropriate concept name based on raw_concepts flag. Args: item: Statement line item dictionary raw_concepts: Whether to use raw XBRL concept names Returns: Concept name (raw or processed) """ if raw_concepts: # Try to get original concept name all_names = item.get('all_names', []) if all_names: # Return first name, converting underscores back to colons for XBRL format original = all_names[0] if '_' in original and ':' not in original: # This looks like a normalized name, try to restore colon format parts = original.split('_', 1) if len(parts) == 2 and parts[0] in ['us-gaap', 'dei', 'srt']: return f"{parts[0]}:{parts[1]}" return original return item.get('concept', '') else: # Use processed concept name return item.get('concept', '') def notes(self, section_name: Optional[str] = None) -> List[Dict[str, Any]]: """ Get notes to financial statements for the current period. Args: section_name: Optional specific note section to retrieve (e.g., "inventory", "revenue recognition") Returns: List of note sections with their content Note: This is a placeholder implementation. Full notes access would require additional development to parse and structure note content. """ # Get all statements and filter for notes all_statements = self.xbrl.get_all_statements() note_statements = [] for stmt in all_statements: stmt_type = (stmt.get('type') or '').lower() definition = (stmt.get('definition') or '').lower() # Check if this looks like a note section if ('note' in stmt_type or 'note' in definition or 'disclosure' in stmt_type or 'disclosure' in definition): # If specific section requested, filter by name if section_name: if section_name.lower() in definition or section_name.lower() in stmt_type: note_statements.append({ 'section_name': stmt.get('definition', 'Untitled Note'), 'type': stmt.get('type', ''), 'role': stmt.get('role', ''), 'element_count': stmt.get('element_count', 0) }) else: # Return all note sections note_statements.append({ 'section_name': stmt.get('definition', 'Untitled Note'), 'type': stmt.get('type', ''), 'role': stmt.get('role', ''), 'element_count': stmt.get('element_count', 0) }) return note_statements def get_fact(self, concept: str, raw_concept: bool = False) -> Any: """ Get a specific fact value for the current period. Args: concept: XBRL concept name to look up raw_concept: If True, treat concept as raw XBRL name (with colons) Returns: Fact value if found, None otherwise Example: >>> revenue = xbrl.current_period.get_fact('Revenues') >>> revenue_raw = xbrl.current_period.get_fact('us-gaap:Revenues', raw_concept=True) """ try: # Normalize concept name if needed if raw_concept and ':' in concept: # Convert colon format to underscore for internal lookup concept = concept.replace(':', '_') # Use XBRL's fact finding method with current period filter facts = self.xbrl._find_facts_for_element(concept, period_filter=self.period_key) if facts: # Return the first matching fact's value for _context_id, wrapped_fact in facts.items(): fact = wrapped_fact['fact'] return fact.numeric_value if fact.numeric_value is not None else fact.value return None except Exception as e: log.debug(f"Error retrieving fact {concept}: {str(e)}") return None def to_dict(self) -> Dict[str, Any]: """ Convert current period data to a dictionary format. Returns: Dictionary with current period information and key financial data """ result = { 'period_key': self.period_key, 'period_label': self.period_label, 'entity_name': getattr(self.xbrl, 'entity_name', None), 'document_type': getattr(self.xbrl, 'document_type', None), 'statements': {} } # Try to get key statements statement_types = ['BalanceSheet', 'IncomeStatement', 'CashFlowStatement'] for stmt_type in statement_types: try: df = self._get_statement_dataframe(stmt_type, raw_concepts=False) if not df.empty: # Convert DataFrame to list of dicts for JSON serialization result['statements'][stmt_type] = df.to_dict('records') except StatementNotFound: result['statements'][stmt_type] = None return result def debug_info(self) -> Dict[str, Any]: """ Get debugging information about the current period and data availability. Returns: Dictionary with detailed debugging information """ info = { 'current_period_key': self.period_key, 'current_period_label': self.period_label, 'total_reporting_periods': len(self.xbrl.reporting_periods), 'entity_name': getattr(self.xbrl, 'entity_name', 'Unknown'), 'document_period_end': getattr(self.xbrl, 'period_of_report', None), 'periods': [], 'statements': {} } # Add all periods with basic info for period in self.xbrl.reporting_periods: period_info = { 'key': period['key'], 'label': period.get('label', 'No label'), 'type': 'instant' if 'instant_' in period['key'] else 'duration' } info['periods'].append(period_info) # Check statement availability statement_types = ['BalanceSheet', 'IncomeStatement', 'CashFlowStatement'] for stmt_type in statement_types: try: # Get the period that would be used for this statement period_for_stmt = self._get_appropriate_period_for_statement(stmt_type) # Get raw statement data raw_data = self.xbrl.get_statement(stmt_type, period_filter=period_for_stmt) if raw_data: # Count items with values items_with_values = sum(1 for item in raw_data if period_for_stmt in item.get('values', {})) info['statements'][stmt_type] = { 'period_used': period_for_stmt, 'raw_data_items': len(raw_data), 'items_with_values': items_with_values, 'available': items_with_values > 0, 'error': None } else: info['statements'][stmt_type] = { 'period_used': period_for_stmt, 'raw_data_items': 0, 'items_with_values': 0, 'available': False, 'error': 'No raw data returned' } except Exception as e: info['statements'][stmt_type] = { 'period_used': None, 'raw_data_items': 0, 'items_with_values': 0, 'available': False, 'error': str(e) } return info def __repr__(self) -> str: """String representation showing current period info.""" entity_name = getattr(self.xbrl, 'entity_name', 'Unknown Entity') return f"CurrentPeriodView(entity='{entity_name}', period='{self.period_label}')" def __str__(self) -> str: """User-friendly string representation.""" entity_name = getattr(self.xbrl, 'entity_name', 'Unknown Entity') return f"Current Period Data for {entity_name}\nPeriod: {self.period_label}" class CurrentPeriodStatement: """ A Statement object that applies current period filtering. This class wraps a regular Statement object and ensures that only the current period data is shown when rendering or accessing data. """ def __init__(self, xbrl, role_or_type: str, canonical_type: Optional[str] = None, period_filter: Optional[str] = None, period_label: Optional[str] = None): """ Initialize with period filtering. Args: xbrl: XBRL object containing parsed data role_or_type: Role URI, statement type, or statement short name canonical_type: Optional canonical statement type period_filter: Period key to filter to period_label: Human-readable period label """ self.xbrl = xbrl self.role_or_type = role_or_type self.canonical_type = canonical_type self.period_filter = period_filter self.period_label = period_label # Create the underlying Statement object from edgar.xbrl.statements import Statement self._statement = Statement(xbrl, role_or_type, canonical_type, skip_concept_check=True) def render(self, standard: bool = True, show_date_range: bool = False, include_dimensions: bool = True) -> Any: """ Render the statement as a formatted table for current period only. Args: standard: Whether to use standardized concept labels show_date_range: Whether to show full date ranges for duration periods include_dimensions: Whether to include dimensional segment data Returns: Rich Table containing the rendered statement for current period """ # Use the canonical type for rendering if available, otherwise use the role rendering_type = self.canonical_type if self.canonical_type else self.role_or_type return self.xbrl.render_statement( rendering_type, period_filter=self.period_filter, standard=standard, show_date_range=show_date_range, include_dimensions=include_dimensions ) def get_raw_data(self) -> List[Dict[str, Any]]: """ Get the raw statement data filtered to current period. Returns: List of line items with values for current period only """ return self._statement.get_raw_data(period_filter=self.period_filter) def get_dataframe(self, raw_concepts: bool = False) -> pd.DataFrame: """ Convert the statement to a DataFrame for current period. Args: raw_concepts: If True, preserve original XBRL concept names Returns: pandas DataFrame with current period data only """ # Get raw data for current period raw_data = self.get_raw_data() # Convert to DataFrame format similar to CurrentPeriodView rows = [] for item in raw_data: values = item.get('values', {}) current_value = values.get(self.period_filter) if current_value is not None: concept_name = item.get('concept', '') if raw_concepts: # Try to get original concept name all_names = item.get('all_names', []) if all_names: original = all_names[0] if '_' in original and ':' not in original: parts = original.split('_', 1) if len(parts) == 2 and parts[0] in ['us-gaap', 'dei', 'srt']: concept_name = f"{parts[0]}:{parts[1]}" else: concept_name = original else: concept_name = original row = { 'concept': concept_name, 'label': item.get('label', ''), 'value': current_value, 'level': item.get('level', 0), 'is_abstract': item.get('is_abstract', False) } # Add original concept name if raw_concepts is requested if raw_concepts: row['standardized_label'] = item.get('label', '') all_names = item.get('all_names', []) if all_names: row['original_concept'] = all_names[0] # Add dimension information if present if item.get('is_dimension', False): row['dimension_label'] = item.get('full_dimension_label', '') row['is_dimension'] = True rows.append(row) return pd.DataFrame(rows) def calculate_ratios(self) -> Dict[str, float]: """Calculate common financial ratios for this statement.""" return self._statement.calculate_ratios() def __rich__(self) -> Any: """Rich console representation.""" return self.render() def __repr__(self) -> str: """String representation.""" return repr_rich(self.__rich__()) def __str__(self) -> str: """User-friendly string representation.""" return repr(self)