Skip to main content

Schedule Schema & Validation

The schedule_schema module serves as the foundational validation layer for tif1’s internal schedule data architecture. This module implements a comprehensive validation system that acts as a critical gatekeeper, ensuring data integrity and structural consistency across all schedule-related operations within the library.

Purpose and Role

The schedule validation system is designed to guarantee that all schedule payloads—regardless of their origin (vendored JSON files, CDN sources, or custom data)—conform precisely to tif1’s expected internal schema before being consumed by higher-level APIs. This validation layer provides several critical functions:
  • Data Integrity Assurance: Validates that schedule data structures are complete, correctly typed, and internally consistent
  • Early Error Detection: Catches malformed or incomplete data at the earliest possible stage, preventing cascading failures
  • Schema Version Management: Ensures compatibility between data format versions and library expectations
  • Type Safety Enforcement: Verifies that all data elements match their expected types throughout the hierarchy
  • Consistency Guarantees: Ensures uniform data structure across all years, events, and data sources
The validation process is designed for performance, typically completing in under 1 millisecond, and integrates seamlessly with tif1’s caching system to ensure validation overhead is minimized in production use.
For Most Users: This module operates transparently behind the scenes. You’ll interact with schedule data through high-level APIs like get_events(), get_sessions(), and get_event_schedule(), which automatically handle validation. Direct use of validation functions is typically only needed for advanced scenarios such as working with custom schedule data, debugging data issues, or extending the library.
Schema Version Compatibility: Currently, only schema version 1 is supported. Attempting to validate payloads with different schema versions will raise an InvalidDataError. Future versions of tif1 may introduce new schema versions with additional features or structural changes.

Architecture Overview

Data Flow Pipeline

The schedule validation system sits at a critical junction in tif1’s data pipeline. Understanding this flow helps clarify when and why validation occurs:
┌─────────────────────────────────────────────────────────────────┐
│                     Schedule Data Sources                        │
├─────────────────────────────────────────────────────────────────┤
│  1. Vendored JSON Files (src/tif1/data/schedules/f1schedule/)   │
│     • Per-year files: schedule_2021.json, schedule_2022.json    │
│     • Columnar format (pandas-like structure)                    │
│     • Bundled with library installation                          │
│                                                                   │
│  2. CDN Fallback (jsdelivr)                                      │
│     • Fetched for years not in vendored data                     │
│     • Same columnar format as vendored files                     │
│     • Cached after first successful fetch                        │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│                    Format Conversion Layer                       │
├─────────────────────────────────────────────────────────────────┤
│  _convert_f1schedule_year()                                      │
│  • Transforms columnar format → event-centric structure          │
│  • Extracts metadata (dates, locations, formats)                 │
│  • Builds sessions mapping                                       │
│  • Sorts events by round number                                  │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│              ★ VALIDATION LAYER (This Module) ★                  │
├─────────────────────────────────────────────────────────────────┤
│  validate_schedule_payload()                                     │
│  • Verifies schema version compatibility                         │
│  • Validates structural integrity                                │
│  • Checks type correctness                                       │
│  • Ensures event-session consistency                             │
│  • Provides detailed error messages                              │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│                      Caching & Storage                           │
├─────────────────────────────────────────────────────────────────┤
│  • Validated payload cached via @lru_cache                       │
│  • Single validation per Python session                          │
│  • Instant access for subsequent requests                        │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│                    High-Level Public APIs                        │
├─────────────────────────────────────────────────────────────────┤
│  • get_events(year) → List of event names                        │
│  • get_sessions(year, event) → List of session names             │
│  • get_event_schedule(year) → EventSchedule DataFrame            │
│  • get_event(year, identifier) → Event object                    │
└─────────────────────────────────────────────────────────────────┘

Key Design Principles

  1. Fail-Fast Philosophy: Validation occurs immediately after data conversion, before any caching or API exposure. This ensures that invalid data never propagates through the system.
  2. Single Validation Point: All schedule data, regardless of source, passes through the same validation function, ensuring consistent quality standards.
  3. Performance Optimization: Validation is designed to be extremely fast (typically <1ms) and runs only once per session due to aggressive caching.
  4. Detailed Error Reporting: When validation fails, the system provides specific, actionable error messages that pinpoint exactly what went wrong and where.
  5. Type Safety: The validation system enforces strict type checking at every level of the data hierarchy, preventing type-related bugs downstream.

Validation Scope

The validation system checks multiple aspects of schedule data:
Validation CategoryWhat It ChecksWhy It Matters
Top-Level StructurePayload is a dictionary with required keysPrevents type errors in downstream code
Schema VersionVersion number is supported (currently only v1)Ensures compatibility with library expectations
Year KeysYear keys are numeric strings (e.g., “2021”)Enables proper sorting and year-based lookups
Events ListEach year has a list of non-empty event namesGuarantees event discovery APIs work correctly
Sessions MappingEach event has a corresponding session listEnsures session queries return valid data
Session NamesAll session names are non-empty stringsPrevents empty or null session references
ConsistencyEvery event in the events list has sessionsMaintains referential integrity

Performance Characteristics

The validation system is optimized for production use:
  • Time Complexity: O(n × m) where n = number of years, m = average events per year
  • Typical Runtime: <1ms for standard multi-year schedules (5-10 years)
  • Memory Overhead: Minimal; validates in-place without copying data structures
  • Caching Strategy: Results cached via @lru_cache; validation runs once per Python session
  • Scalability: Handles schedules with 20+ events per year efficiently
Benchmark Example: Validating a 5-year schedule (2020-2024) with ~22 events per year and ~5 sessions per event typically completes in 0.3-0.8ms on modern hardware.

Core API Reference

validate_schedule_payload

def validate_schedule_payload(payload: Any) -> dict[str, Any]
The primary and only public validation function in this module. This function performs comprehensive, hierarchical validation of schedule payloads to ensure they conform to tif1’s internal schema specification version 1.

Function Signature

Parameters:
  • payload (Any): The decoded schedule payload to validate. While typed as Any to accept arbitrary input, the function expects a dictionary with the following structure:
    {
        "schema_version": int,  # Must be exactly 1
        "years": {
            "YYYY": {  # Year as string, e.g., "2021", "2024"
                "events": [str, ...],  # List of event names
                "sessions": {
                    "Event Name": [str, ...]  # Session names for each event
                },
                "metadata": {  # Optional metadata dictionary
                    "Event Name": {
                        "RoundNumber": int,
                        "EventDate": str,
                        "Location": str,
                        # ... additional metadata fields
                    }
                }
            }
        }
    }
    
Returns:
  • dict[str, Any]: The validated payload, returned unchanged if all validation checks pass. This design allows for method chaining and confirms that the payload is safe for use in downstream operations. The return value is guaranteed to match the expected schema structure.
Raises:
  • InvalidDataError: Raised when any validation check fails. The exception includes:
    • message: Human-readable description of the validation failure
    • context: Dictionary containing structured error information, including:
      • reason: Detailed explanation of what validation rule was violated
      • Additional context-specific fields depending on the failure type

Validation Process

The function performs validation in a hierarchical, top-down manner, checking each level of the data structure before proceeding to the next:
Level 1: Top-Level Structure Validation
# Checks performed:
- payload is a dictionary (not list, string, None, etc.)
- payload contains "schema_version" key
- payload contains "years" key
Failure Example:
# Invalid: payload is a list
payload = [{"year": 2021}]
# Raises: InvalidDataError(reason="Schedule payload must be an object")

# Invalid: missing required keys
payload = {"data": {...}}
# Raises: InvalidDataError(reason="Schedule payload missing 'years' object")
Level 2: Schema Version Validation
# Checks performed:
- schema_version exists
- schema_version is exactly 1 (only supported version)
Failure Example:
# Invalid: unsupported version
payload = {"schema_version": 2, "years": {...}}
# Raises: InvalidDataError(reason="Unsupported schedule schema version: 2")

# Invalid: missing version
payload = {"years": {...}}
# Raises: InvalidDataError(reason="Unsupported schedule schema version: None")
Level 3: Years Container Validation
# Checks performed:
- "years" value is a dictionary
- Each year key is a string of digits (e.g., "2021", not 2021 or "twenty-one")
- Each year value is a dictionary
Failure Example:
# Invalid: year key is not a string of digits
payload = {
    "schema_version": 1,
    "years": {
        "twenty-twenty-one": {...}  # Should be "2021"
    }
}
# Raises: InvalidDataError(reason="Invalid year key: 'twenty-twenty-one'")

# Invalid: year value is not a dictionary
payload = {
    "schema_version": 1,
    "years": {
        "2021": ["event1", "event2"]  # Should be a dict
    }
}
# Raises: InvalidDataError(reason="Year payload must be object for year=2021")
Level 4: Per-Year Structure Validation
# For each year, checks:
- "events" key exists and is a list
- All items in "events" list are non-empty strings
- "sessions" key exists and is a dictionary
Failure Example:
# Invalid: events is not a list
payload = {
    "schema_version": 1,
    "years": {
        "2021": {
            "events": "Bahrain Grand Prix",  # Should be a list
            "sessions": {...}
        }
    }
}
# Raises: InvalidDataError(reason="Invalid events list for year=2021")

# Invalid: event name is empty
payload = {
    "schema_version": 1,
    "years": {
        "2021": {
            "events": ["Bahrain Grand Prix", ""],  # Empty string not allowed
            "sessions": {...}
        }
    }
}
# Raises: InvalidDataError(reason="Invalid events list for year=2021")
Level 5: Session Mapping Validation
# For each event in each year, checks:
- Event name exists as a key in the "sessions" dictionary
- Session list for the event is a list
- All session names in the list are non-empty strings
Failure Example:
# Invalid: missing session list for an event
payload = {
    "schema_version": 1,
    "years": {
        "2021": {
            "events": ["Bahrain Grand Prix", "Belgian Grand Prix"],
            "sessions": {
                "Bahrain Grand Prix": ["Practice 1", "Qualifying", "Race"]
                # Missing "Belgian Grand Prix" entry!
            }
        }
    }
}
# Raises: InvalidDataError(
#     reason="Invalid session list for year=2021 event='Belgian Grand Prix'"
# )

# Invalid: session list contains non-string
payload = {
    "schema_version": 1,
    "years": {
        "2021": {
            "events": ["Bahrain Grand Prix"],
            "sessions": {
                "Bahrain Grand Prix": ["Practice 1", None, "Race"]  # None not allowed
            }
        }
    }
}
# Raises: InvalidDataError(
#     reason="Invalid session list for year=2021 event='Bahrain Grand Prix'"
# )

Usage Examples

Example 1: Basic Validation (Success Case)
from tif1.schedule_schema import validate_schedule_payload

# Construct a valid payload
payload = {
    "schema_version": 1,
    "years": {
        "2021": {
            "events": ["Bahrain Grand Prix", "Belgian Grand Prix"],
            "sessions": {
                "Bahrain Grand Prix": [
                    "Practice 1", "Practice 2", "Practice 3",
                    "Qualifying", "Race"
                ],
                "Belgian Grand Prix": [
                    "Practice 1", "Practice 2", "Practice 3",
                    "Qualifying", "Race"
                ]
            }
        }
    }
}

# Validate the payload
validated = validate_schedule_payload(payload)

# If we reach here, validation succeeded
print(f"✓ Schedule is valid")
print(f"  Years: {list(validated['years'].keys())}")
print(f"  Events in 2021: {len(validated['years']['2021']['events'])}")

# Output:
# ✓ Schedule is valid
#   Years: ['2021']
#   Events in 2021: 2
Example 2: Handling Validation Errors
from tif1.schedule_schema import validate_schedule_payload
from tif1.exceptions import InvalidDataError

# Invalid payload - missing sessions for an event
invalid_payload = {
    "schema_version": 1,
    "years": {
        "2021": {
            "events": ["Bahrain Grand Prix", "Belgian Grand Prix"],
            "sessions": {
                "Bahrain Grand Prix": ["Practice 1", "Qualifying", "Race"]
                # Missing "Belgian Grand Prix" sessions!
            }
        }
    }
}

try:
    validate_schedule_payload(invalid_payload)
    print("Validation succeeded")  # Won't reach here
except InvalidDataError as e:
    print(f"✗ Validation failed!")
    print(f"  Error: {e.message}")
    print(f"  Reason: {e.context.get('reason')}")

    # You can also access the full context
    print(f"  Full context: {e.context}")

# Output:
# ✗ Validation failed!
#   Error: Invalid data: Invalid session list for year=2021 event='Belgian Grand Prix'
#   Reason: Invalid session list for year=2021 event='Belgian Grand Prix'
#   Full context: {'reason': "Invalid session list for year=2021 event='Belgian Grand Prix'"}
Example 3: Multi-Year Validation
from tif1.schedule_schema import validate_schedule_payload

# Validate multiple years at once
multi_year_payload = {
    "schema_version": 1,
    "years": {
        "2021": {
            "events": ["Bahrain Grand Prix", "Abu Dhabi Grand Prix"],
            "sessions": {
                "Bahrain Grand Prix": [
                    "Practice 1", "Practice 2", "Practice 3",
                    "Qualifying", "Race"
                ],
                "Abu Dhabi Grand Prix": [
                    "Practice 1", "Practice 2", "Practice 3",
                    "Qualifying", "Race"
                ]
            }
        },
        "2022": {
            "events": ["Bahrain Grand Prix", "Saudi Arabian Grand Prix"],
            "sessions": {
                "Bahrain Grand Prix": [
                    "Practice 1", "Practice 2", "Practice 3",
                    "Qualifying", "Race"
                ],
                "Saudi Arabian Grand Prix": [
                    "Practice 1", "Practice 2", "Practice 3",
                    "Qualifying", "Race"
                ]
            }
        },
        "2023": {
            "events": ["Bahrain Grand Prix", "Saudi Arabian Grand Prix", "Australian Grand Prix"],
            "sessions": {
                "Bahrain Grand Prix": [
                    "Practice 1", "Practice 2", "Practice 3",
                    "Qualifying", "Race"
                ],
                "Saudi Arabian Grand Prix": [
                    "Practice 1", "Practice 2", "Practice 3",
                    "Qualifying", "Race"
                ],
                "Australian Grand Prix": [
                    "Practice 1", "Practice 2", "Practice 3",
                    "Qualifying", "Race"
                ]
            }
        }
    }
}

validated = validate_schedule_payload(multi_year_payload)

print(f"✓ Validated {len(validated['years'])} years of schedule data")
for year in sorted(validated['years'].keys()):
    events_count = len(validated['years'][year]['events'])
    print(f"  {year}: {events_count} events")

# Output:
# ✓ Validated 3 years of schedule data
#   2021: 2 events
#   2022: 2 events
#   2023: 3 events
Example 4: Validating Sprint Weekend Format
from tif1.schedule_schema import validate_schedule_payload

# Sprint weekends have different session structures
sprint_payload = {
    "schema_version": 1,
    "years": {
        "2021": {
            "events": ["British Grand Prix"],  # Sprint weekend
            "sessions": {
                "British Grand Prix": [
                    "Practice 1",      # Friday
                    "Qualifying",      # Friday (for Sunday grid)
                    "Practice 2",      # Saturday
                    "Sprint",          # Saturday (sets Sunday grid)
                    "Race"             # Sunday
                ]
            }
        }
    }
}

validated = validate_schedule_payload(sprint_payload)
sessions = validated['years']['2021']['sessions']['British Grand Prix']

print(f"✓ Sprint weekend validated")
print(f"  Sessions: {', '.join(sessions)}")
print(f"  Total sessions: {len(sessions)}")

# Output:
# ✓ Sprint weekend validated
#   Sessions: Practice 1, Qualifying, Practice 2, Sprint, Race
#   Total sessions: 5
Example 5: Validation with Metadata (Optional Fields)
from tif1.schedule_schema import validate_schedule_payload

# Metadata is optional and not validated by this function
# (it's validated by higher-level code if present)
payload_with_metadata = {
    "schema_version": 1,
    "years": {
        "2021": {
            "events": ["Belgian Grand Prix"],
            "sessions": {
                "Belgian Grand Prix": [
                    "Practice 1", "Practice 2", "Practice 3",
                    "Qualifying", "Race"
                ]
            },
            "metadata": {  # Optional metadata section
                "Belgian Grand Prix": {
                    "RoundNumber": 12,
                    "EventDate": "2021-08-29T17:00:00",
                    "Location": "Spa-Francorchamps",
                    "Country": "Belgium",
                    "OfficialEventName": "FORMULA 1 ROLEX BELGIAN GRAND PRIX 2021",
                    "EventFormat": "conventional",
                    "GmtOffset": "+02:00",
                    "F1ApiSupport": True,
                    "Session1Date": "2021-08-27T11:30:00",
                    "Session2Date": "2021-08-27T15:00:00",
                    "Session3Date": "2021-08-28T12:00:00",
                    "Session4Date": "2021-08-28T15:00:00",
                    "Session5Date": "2021-08-29T15:00:00"
                }
            }
        }
    }
}

validated = validate_schedule_payload(payload_with_metadata)

print(f"✓ Payload with metadata validated")
print(f"  Metadata fields: {list(validated['years']['2021']['metadata']['Belgian Grand Prix'].keys())}")

# Output:
# ✓ Payload with metadata validated
#   Metadata fields: ['RoundNumber', 'EventDate', 'Location', 'Country', ...]
Example 6: Error Recovery Pattern
from tif1.schedule_schema import validate_schedule_payload
from tif1.exceptions import InvalidDataError

def load_and_validate_schedule(payload):
    """
    Attempt to validate a schedule payload with error recovery.

    Returns:
        tuple: (success: bool, validated_payload or error_message)
    """
    try:
        validated = validate_schedule_payload(payload)
        return True, validated
    except InvalidDataError as e:
        error_info = {
            "message": e.message,
            "reason": e.context.get("reason"),
            "context": e.context
        }
        return False, error_info

# Test with valid payload
valid_payload = {
    "schema_version": 1,
    "years": {
        "2021": {
            "events": ["Bahrain Grand Prix"],
            "sessions": {
                "Bahrain Grand Prix": ["Practice 1", "Qualifying", "Race"]
            }
        }
    }
}

success, result = load_and_validate_schedule(valid_payload)
if success:
    print(f"✓ Validation succeeded")
    print(f"  Years available: {list(result['years'].keys())}")
else:
    print(f"✗ Validation failed: {result['reason']}")

# Test with invalid payload
invalid_payload = {
    "schema_version": 1,
    "years": {
        "2021": {
            "events": ["Bahrain Grand Prix"],
            "sessions": {}  # Missing session list!
        }
    }
}

success, result = load_and_validate_schedule(invalid_payload)
if success:
    print(f"✓ Validation succeeded")
else:
    print(f"✗ Validation failed: {result['reason']}")

# Output:
# ✓ Validation succeeded
#   Years available: ['2021']
# ✗ Validation failed: Invalid session list for year=2021 event='Bahrain Grand Prix'

Common Validation Failures

Here’s a comprehensive reference of common validation failures and how to fix them:
Error MessageCauseSolution
Schedule payload must be an objectPayload is not a dictionaryEnsure payload is a dict, not list/string/None
Unsupported schedule schema version: XSchema version is not 1Set schema_version to 1
Schedule payload missing 'years' objectMissing or invalid years keyAdd "years": {} dictionary to payload
Invalid year key: 'X'Year key is not a numeric stringUse "2021" not 2021 or "twenty-one"
Year payload must be object for year=XYear value is not a dictionaryEnsure each year maps to a dictionary
Invalid events list for year=Xevents is missing, not a list, or contains non-stringsProvide "events": ["Event 1", "Event 2"]
Invalid sessions map for year=Xsessions is missing or not a dictionaryProvide "sessions": {} dictionary
Invalid session list for year=X event='Y'Session list missing, not a list, or contains non-stringsAdd "Event Name": ["Session 1", "Session 2"]

Performance Considerations

The validation function is optimized for production use:
  • Fast Execution: Typical validation time is 0.3-0.8ms for multi-year schedules
  • No Data Copying: Validates in-place without creating copies of the data structure
  • Early Exit: Stops at the first validation failure, avoiding unnecessary checks
  • Minimal Allocations: Uses efficient iteration patterns to minimize memory allocations
Benchmark Results (on modern hardware):
Schedule SizeEventsSessionsValidation Time
1 year22 events~110 sessions~0.2ms
5 years110 events~550 sessions~0.6ms
10 years220 events~1100 sessions~1.1ms
Integration Tip: When building custom schedule loaders or data pipelines, call validate_schedule_payload() immediately after constructing your payload and before any caching or API exposure. This ensures data quality at the earliest possible stage.

Schedule Schema Specification

Schema Structure

The internal schedule schema is designed to be event-centric, optimizing for the most common query patterns in Formula 1 data analysis. This structure differs from the raw f1schedule format (which uses a columnar layout) to provide better performance for event and session lookups.

Complete Schema Definition

{
  "schema_version": 1,
  "years": {
    "YYYY": {
      "events": [
        "Event Name 1",
        "Event Name 2",
        "..."
      ],
      "sessions": {
        "Event Name 1": [
          "Session 1",
          "Session 2",
          "..."
        ],
        "Event Name 2": [
          "Session 1",
          "Session 2",
          "..."
        ]
      },
      "metadata": {
        "Event Name 1": {
          "RoundNumber": 1,
          "EventDate": "YYYY-MM-DDTHH:MM:SS",
          "Location": "City/Circuit Name",
          "Country": "Country Name",
          "OfficialEventName": "FORMULA 1 OFFICIAL EVENT NAME YYYY",
          "EventFormat": "conventional|sprint|testing",
          "GmtOffset": "+HH:MM",
          "F1ApiSupport": true|false,
          "Session1Date": "YYYY-MM-DDTHH:MM:SS",
          "Session2Date": "YYYY-MM-DDTHH:MM:SS",
          "Session3Date": "YYYY-MM-DDTHH:MM:SS",
          "Session4Date": "YYYY-MM-DDTHH:MM:SS",
          "Session5Date": "YYYY-MM-DDTHH:MM:SS",
          "Session1DateUtc": "YYYY-MM-DDTHH:MM:SS",
          "Session2DateUtc": "YYYY-MM-DDTHH:MM:SS",
          "Session3DateUtc": "YYYY-MM-DDTHH:MM:SS",
          "Session4DateUtc": "YYYY-MM-DDTHH:MM:SS",
          "Session5DateUtc": "YYYY-MM-DDTHH:MM:SS"
        }
      }
    }
  }
}

Real-World Example: 2021 Belgian Grand Prix

{
  "schema_version": 1,
  "years": {
    "2021": {
      "events": [
        "Bahrain Grand Prix",
        "Emilia Romagna Grand Prix",
        "Portuguese Grand Prix",
        "Spanish Grand Prix",
        "Monaco Grand Prix",
        "Azerbaijan Grand Prix",
        "French Grand Prix",
        "Styrian Grand Prix",
        "Austrian Grand Prix",
        "British Grand Prix",
        "Hungarian Grand Prix",
        "Belgian Grand Prix",
        "Dutch Grand Prix",
        "Italian Grand Prix",
        "Russian Grand Prix",
        "Turkish Grand Prix",
        "United States Grand Prix",
        "Mexico City Grand Prix",
        "São Paulo Grand Prix",
        "Qatar Grand Prix",
        "Saudi Arabian Grand Prix",
        "Abu Dhabi Grand Prix"
      ],
      "sessions": {
        "Belgian Grand Prix": [
          "Practice 1",
          "Practice 2",
          "Practice 3",
          "Qualifying",
          "Race"
        ]
      },
      "metadata": {
        "Belgian Grand Prix": {
          "RoundNumber": 12,
          "EventDate": "2021-08-29T17:00:00",
          "Location": "Spa-Francorchamps",
          "Country": "Belgium",
          "OfficialEventName": "FORMULA 1 ROLEX BELGIAN GRAND PRIX 2021",
          "EventFormat": "conventional",
          "GmtOffset": "+02:00",
          "F1ApiSupport": true,
          "Session1Date": "2021-08-27T11:30:00",
          "Session2Date": "2021-08-27T15:00:00",
          "Session3Date": "2021-08-28T12:00:00",
          "Session4Date": "2021-08-28T15:00:00",
          "Session5Date": "2021-08-29T15:00:00",
          "Session1DateUtc": "2021-08-27T09:30:00",
          "Session2DateUtc": "2021-08-27T13:00:00",
          "Session3DateUtc": "2021-08-28T10:00:00",
          "Session4DateUtc": "2021-08-28T13:00:00",
          "Session5DateUtc": "2021-08-29T13:00:00"
        }
      }
    }
  }
}

Field Specifications

Top-Level Fields

FieldTypeRequiredDescription
schema_versionintSchema format version. Must be 1. Future versions may introduce breaking changes.
yearsdict[str, YearPayload]Dictionary mapping year strings to year-specific schedule data. Keys must be numeric strings (e.g., “2021”).

Year Payload Fields

FieldTypeRequiredDescription
eventslist[str]Ordered list of event names for the year. Order typically follows championship calendar (race events by round number, then testing events by date).
sessionsdict[str, list[str]]Maps each event name to its list of session names. Every event in events must have a corresponding entry.
metadatadict[str, EventMetadata]Optional metadata for each event. Not validated by validate_schedule_payload() but used by higher-level APIs.

Event Metadata Fields

FieldTypeRequiredDescriptionExample
RoundNumberintChampionship round number. 0 for testing events.12
EventDatestrMain event date (typically race day) in ISO 8601 format (local time)."2021-08-29T17:00:00"
LocationstrCircuit or city name."Spa-Francorchamps"
CountrystrCountry where the event takes place."Belgium"
OfficialEventNamestrFull official FIA event name."FORMULA 1 ROLEX BELGIAN GRAND PRIX 2021"
EventFormatstrEvent format type. Values: "conventional", "sprint", "testing"."conventional"
GmtOffsetstrLocal timezone offset from UTC."+02:00"
F1ApiSupportboolWhether the event is supported by F1’s official API.true
Session1DatestrDate/time of first session in local time (ISO 8601)."2021-08-27T11:30:00"
Session2DatestrDate/time of second session in local time."2021-08-27T15:00:00"
Session3DatestrDate/time of third session in local time."2021-08-28T12:00:00"
Session4DatestrDate/time of fourth session in local time."2021-08-28T15:00:00"
Session5DatestrDate/time of fifth session in local time."2021-08-29T15:00:00"
Session1DateUtcstrDate/time of first session in UTC (ISO 8601)."2021-08-27T09:30:00"
Session2DateUtcstrDate/time of second session in UTC."2021-08-27T13:00:00"
Session3DateUtcstrDate/time of third session in UTC."2021-08-28T10:00:00"
Session4DateUtcstrDate/time of fourth session in UTC."2021-08-28T13:00:00"
Session5DateUtcstrDate/time of fifth session in UTC."2021-08-29T13:00:00"

Session Name Standards

Session names follow standardized conventions:

Standard Session Names

Session NameAbbreviationDescriptionTypical Day
Practice 1FP1First practice sessionFriday
Practice 2FP2Second practice sessionFriday
Practice 3FP3Third practice sessionSaturday
QualifyingQQualifying session (sets grid for race)Saturday
RaceRMain raceSunday

Sprint Weekend Session Names

Sprint weekends have a different structure:
Session NameAbbreviationDescriptionTypical Day
Practice 1FP1First practice sessionFriday
QualifyingQQualifying (sets grid for Sunday race)Friday
Practice 2FP2Second practice session (Sprint Shootout in 2023+)Saturday
SprintSSprint race (sets Sunday grid in 2021-2022)Saturday
Sprint ShootoutSSSprint qualifying (sets Sprint grid, 2023+)Saturday
Sprint QualifyingSQSprint qualifying (2021-2022, same as Sprint)Saturday
RaceRMain raceSunday
Sprint Format Evolution: The sprint weekend format has evolved over the years:
  • 2021-2022: Used “Sprint Qualifying” (later renamed to just “Sprint”)
  • 2023+: Introduced “Sprint Shootout” as a separate qualifying session for the Sprint race
tif1 handles these variations automatically through backward compatibility logic in the Event.get_session_name() method.

Event Format Types

The EventFormat metadata field indicates the weekend structure:
FormatDescriptionTypical SessionsExample Events
conventionalStandard race weekendFP1, FP2, FP3, Qualifying, RaceMost Grand Prix events
sprintSprint race weekendFP1, Qualifying, FP2/SS, Sprint, RaceBritish GP 2021, Italian GP 2021
testingPre-season or in-season testingPractice 1, Practice 2, Practice 3Pre-Season Test 2021

Schema Version History

Version 1 (Current)

  • Introduced: tif1 v0.1.0
  • Status: Current and only supported version
  • Features:
    • Event-centric structure
    • Support for conventional, sprint, and testing formats
    • Metadata with timezone information
    • Session date tracking (local and UTC)

Future Versions (Planned)

Future schema versions may include:
  • Version 2 (Tentative):
    • Circuit information (length, corners, DRS zones)
    • Weather forecast data
    • Tire compound allocations
    • Support for new session types (e.g., “Sprint Shootout”)
    • Enhanced metadata for special events
Breaking Changes: When new schema versions are introduced, they will be clearly documented with migration guides. The library will maintain backward compatibility where possible, but validation will require explicit version support.

Validation Rules Reference

This section provides a comprehensive reference of all validation rules enforced by validate_schedule_payload().

Rule Categories

Category 1: Structural Validation

These rules ensure the payload has the correct overall structure:
Rule IDCheckFailure ConditionError Message
S1Payload typePayload is not a dict"Schedule payload must be an object"
S2Schema version presenceschema_version key missing"Unsupported schedule schema version: None"
S3Schema version valueschema_version is not 1"Unsupported schedule schema version: {value}"
S4Years container presenceyears key missing"Schedule payload missing 'years' object"
S5Years container typeyears is not a dict"Schedule payload missing 'years' object"

Category 2: Year-Level Validation

These rules validate each year entry in the years dictionary:
Rule IDCheckFailure ConditionError Message
Y1Year key formatYear key is not a string of digits"Invalid year key: {key!r}"
Y2Year payload typeYear value is not a dict"Year payload must be object for year={year}"
Y3Events list presenceevents key missing or not a list"Invalid events list for year={year}"
Y4Events list contentAny event in list is not a string"Invalid events list for year={year}"
Y5Events list non-emptyAny event string is empty"Invalid events list for year={year}"
Y6Sessions map presencesessions key missing or not a dict"Invalid sessions map for year={year}"

Category 3: Session-Level Validation

These rules validate session mappings for each event:
Rule IDCheckFailure ConditionError Message
E1Session list presenceEvent has no entry in sessions dict"Invalid session list for year={year} event={event!r}"
E2Session list typeSession list is not a list"Invalid session list for year={year} event={event!r}"
E3Session list contentAny session is not a string"Invalid session list for year={year} event={event!r}"
E4Session list non-emptyAny session string is empty"Invalid session list for year={year} event={event!r}"

Validation Order

The validation process follows this exact order:
1. Check payload is dict (S1)
2. Check schema_version exists and equals 1 (S2, S3)
3. Check years exists and is dict (S4, S5)
4. For each year in years:
   a. Check year key format (Y1)
   b. Check year payload is dict (Y2)
   c. Check events list exists and is valid (Y3, Y4, Y5)
   d. Check sessions dict exists (Y6)
   e. For each event in events list:
      i. Check session list exists and is valid (E1, E2, E3, E4)
Early Exit Behavior: Validation stops at the first rule violation. This means you’ll only see one error at a time, even if multiple issues exist. Fix the reported error and re-validate to discover any additional issues.

Common Validation Scenarios

Scenario 1: Empty Payload

payload = {}

# Fails at: S2 (schema_version missing)
# Error: "Unsupported schedule schema version: None"
Fix:
payload = {
    "schema_version": 1,
    "years": {}
}
# ✓ Valid (empty schedule)

Scenario 2: Wrong Data Type

payload = [
    {"year": 2021, "events": ["Bahrain Grand Prix"]}
]

# Fails at: S1 (payload not a dict)
# Error: "Schedule payload must be an object"
Fix:
payload = {
    "schema_version": 1,
    "years": {
        "2021": {
            "events": ["Bahrain Grand Prix"],
            "sessions": {
                "Bahrain Grand Prix": ["Practice 1", "Qualifying", "Race"]
            }
        }
    }
}
# ✓ Valid

Scenario 3: Invalid Year Key

payload = {
    "schema_version": 1,
    "years": {
        2021: {  # Integer instead of string
            "events": ["Bahrain Grand Prix"],
            "sessions": {"Bahrain Grand Prix": ["Race"]}
        }
    }
}

# Fails at: Y1 (year key not a string)
# Error: "Invalid year key: 2021"
Fix:
payload = {
    "schema_version": 1,
    "years": {
        "2021": {  # String key
            "events": ["Bahrain Grand Prix"],
            "sessions": {"Bahrain Grand Prix": ["Race"]}
        }
    }
}
# ✓ Valid

Scenario 4: Missing Session Mapping

payload = {
    "schema_version": 1,
    "years": {
        "2021": {
            "events": ["Bahrain Grand Prix", "Belgian Grand Prix"],
            "sessions": {
                "Bahrain Grand Prix": ["Practice 1", "Qualifying", "Race"]
                # Missing "Belgian Grand Prix"!
            }
        }
    }
}

# Fails at: E1 (session list missing for event)
# Error: "Invalid session list for year=2021 event='Belgian Grand Prix'"
Fix:
payload = {
    "schema_version": 1,
    "years": {
        "2021": {
            "events": ["Bahrain Grand Prix", "Belgian Grand Prix"],
            "sessions": {
                "Bahrain Grand Prix": ["Practice 1", "Qualifying", "Race"],
                "Belgian Grand Prix": ["Practice 1", "Qualifying", "Race"]  # Added
            }
        }
    }
}
# ✓ Valid

Scenario 5: Empty Session List

payload = {
    "schema_version": 1,
    "years": {
        "2021": {
            "events": ["Bahrain Grand Prix"],
            "sessions": {
                "Bahrain Grand Prix": []  # Empty list
            }
        }
    }
}

# ✓ Valid (empty session lists are allowed)
Empty Lists: While empty event lists and empty session lists pass validation, they may cause issues in higher-level APIs that expect at least one session per event. The validation layer only checks structural correctness, not business logic constraints.

Scenario 6: Invalid Session Type

payload = {
    "schema_version": 1,
    "years": {
        "2021": {
            "events": ["Bahrain Grand Prix"],
            "sessions": {
                "Bahrain Grand Prix": ["Practice 1", None, "Race"]  # None not allowed
            }
        }
    }
}

# Fails at: E3 (session not a string)
# Error: "Invalid session list for year=2021 event='Bahrain Grand Prix'"
Fix:
payload = {
    "schema_version": 1,
    "years": {
        "2021": {
            "events": ["Bahrain Grand Prix"],
            "sessions": {
                "Bahrain Grand Prix": ["Practice 1", "Qualifying", "Race"]  # All strings
            }
        }
    }
}
# ✓ Valid

Validation Best Practices

1. Validate Early

Always validate immediately after constructing or loading schedule data:
from tif1.schedule_schema import validate_schedule_payload
from tif1.exceptions import InvalidDataError

def load_custom_schedule(file_path):
    """Load and validate a custom schedule file."""
    import json

    with open(file_path) as f:
        payload = json.load(f)

    # Validate immediately after loading
    try:
        validated = validate_schedule_payload(payload)
        return validated
    except InvalidDataError as e:
        raise ValueError(f"Invalid schedule file: {e.message}") from e

2. Provide Context in Errors

When wrapping validation, preserve error context:
from tif1.schedule_schema import validate_schedule_payload
from tif1.exceptions import InvalidDataError

def validate_with_context(payload, source_name):
    """Validate with additional context."""
    try:
        return validate_schedule_payload(payload)
    except InvalidDataError as e:
        # Preserve original error context
        raise InvalidDataError(
            reason=f"Validation failed for {source_name}: {e.context.get('reason')}",
            source=source_name,
            original_error=e.message
        ) from e

3. Build Payloads Incrementally

When constructing complex payloads, validate at each stage:
from tif1.schedule_schema import validate_schedule_payload

# Start with minimal valid structure
payload = {
    "schema_version": 1,
    "years": {}
}

# Validate base structure
validate_schedule_payload(payload)  # ✓ Valid

# Add years incrementally
for year in [2021, 2022, 2023]:
    payload["years"][str(year)] = {
        "events": [],
        "sessions": {}
    }
    # Validate after each addition
    validate_schedule_payload(payload)  # ✓ Valid

# Add events and sessions
for year_str in payload["years"]:
    payload["years"][year_str]["events"] = ["Bahrain Grand Prix"]
    payload["years"][year_str]["sessions"] = {
        "Bahrain Grand Prix": ["Practice 1", "Qualifying", "Race"]
    }
    # Validate after modifications
    validate_schedule_payload(payload)  # ✓ Valid

4. Handle Validation in Pipelines

For data processing pipelines, use validation as a quality gate:
from tif1.schedule_schema import validate_schedule_payload
from tif1.exceptions import InvalidDataError

def process_schedule_pipeline(raw_data):
    """Process schedule data with validation gates."""

    # Stage 1: Parse raw data
    parsed = parse_raw_schedule(raw_data)

    # Stage 2: Transform to internal format
    transformed = transform_to_internal_format(parsed)

    # Stage 3: Validate (quality gate)
    try:
        validated = validate_schedule_payload(transformed)
    except InvalidDataError as e:
        # Log validation failure and halt pipeline
        logger.error(f"Schedule validation failed: {e.message}")
        raise

    # Stage 4: Cache and return (only if validation passed)
    cache_schedule(validated)
    return validated

Error Handling

Exception Hierarchy

Schedule validation errors are part of tif1’s exception hierarchy:
TIF1Error (base exception)
└── InvalidDataError
    └── Raised by validate_schedule_payload()

InvalidDataError Structure

When validation fails, an InvalidDataError is raised with the following structure:
class InvalidDataError(TIF1Error):
    def __init__(self, reason: str | None = None, **context: Any):
        message = f"Invalid data: {reason}" if reason else "Invalid or corrupted data"
        super().__init__(message, reason=reason, **context)
Attributes:
  • message (str): Human-readable error message
  • context (dict[str, Any]): Structured error context containing:
    • reason (str): Detailed explanation of the validation failure
    • Additional context fields (varies by error type)

Error Message Patterns

All validation error messages follow consistent patterns:
PatternExampleMeaning
Schedule payload must be an objectExact messageTop-level structure error
Unsupported schedule schema version: {version}...version: 2Schema version mismatch
Schedule payload missing 'years' objectExact messageMissing years container
Invalid year key: {key!r}...key: 'twenty-one'Year key format error
Year payload must be object for year={year}...year=2021Year value type error
Invalid events list for year={year}...year=2021Events list error
Invalid sessions map for year={year}...year=2021Sessions dict error
Invalid session list for year={year} event={event!r}...year=2021 event='Belgian Grand Prix'Session list error

Catching and Handling Errors

Basic Error Handling

from tif1.schedule_schema import validate_schedule_payload
from tif1.exceptions import InvalidDataError

try:
    validated = validate_schedule_payload(payload)
    print("✓ Validation succeeded")
except InvalidDataError as e:
    print(f"✗ Validation failed: {e.message}")
    print(f"  Reason: {e.context.get('reason')}")

Detailed Error Inspection

from tif1.schedule_schema import validate_schedule_payload
from tif1.exceptions import InvalidDataError

try:
    validated = validate_schedule_payload(payload)
except InvalidDataError as e:
    # Access all error information
    print(f"Error Type: {type(e).__name__}")
    print(f"Message: {e.message}")
    print(f"Reason: {e.context.get('reason')}")
    print(f"Full Context: {e.context}")

    # Check for specific error patterns
    if "schema version" in e.message.lower():
        print("→ Schema version issue detected")
    elif "year=" in e.message:
        print("→ Year-level validation issue")
    elif "event=" in e.message:
        print("→ Event-level validation issue")

Error Recovery Strategies

from tif1.schedule_schema import validate_schedule_payload
from tif1.exceptions import InvalidDataError

def load_schedule_with_fallback(primary_payload, fallback_payload):
    """
    Attempt to load primary payload, fall back to secondary if validation fails.
    """
    try:
        return validate_schedule_payload(primary_payload)
    except InvalidDataError as e:
        logger.warning(f"Primary payload validation failed: {e.message}")
        logger.info("Attempting fallback payload...")

        try:
            return validate_schedule_payload(fallback_payload)
        except InvalidDataError as e2:
            logger.error(f"Fallback payload also failed: {e2.message}")
            raise ValueError("Both primary and fallback payloads are invalid") from e2

Logging Validation Errors

import logging
from tif1.schedule_schema import validate_schedule_payload
from tif1.exceptions import InvalidDataError

logger = logging.getLogger(__name__)

def validate_and_log(payload, source_name):
    """Validate payload with comprehensive logging."""
    try:
        validated = validate_schedule_payload(payload)
        logger.info(f"✓ Validated schedule from {source_name}")
        logger.debug(f"  Years: {list(validated['years'].keys())}")
        return validated
    except InvalidDataError as e:
        logger.error(
            f"✗ Schedule validation failed for {source_name}",
            extra={
                "source": source_name,
                "error_message": e.message,
                "error_reason": e.context.get("reason"),
                "error_context": e.context
            }
        )
        raise

Custom Error Messages

from tif1.schedule_schema import validate_schedule_payload
from tif1.exceptions import InvalidDataError

def validate_with_custom_errors(payload, context_info):
    """Validate with application-specific error messages."""
    try:
        return validate_schedule_payload(payload)
    except InvalidDataError as e:
        # Transform technical error into user-friendly message
        user_message = _create_user_friendly_message(e, context_info)

        # Re-raise with custom message but preserve original error
        raise ValueError(user_message) from e

def _create_user_friendly_message(error, context):
    """Convert technical validation error to user-friendly message."""
    reason = error.context.get("reason", "")

    if "schema version" in reason.lower():
        return (
            f"The schedule file format is not supported. "
            f"Please ensure you're using a compatible schedule file. "
            f"(Technical: {reason})"
        )
    elif "year=" in reason:
        return (
            f"There's an issue with the schedule data for a specific year. "
            f"Please check the schedule file structure. "
            f"(Technical: {reason})"
        )
    elif "event=" in reason:
        return (
            f"There's an issue with session data for a specific event. "
            f"Please ensure all events have valid session lists. "
            f"(Technical: {reason})"
        )
    else:
        return (
            f"The schedule file structure is invalid. "
            f"Please check the file format. "
            f"(Technical: {reason})"
        )

Debugging Validation Failures

Strategy 1: Incremental Validation

Build and validate the payload incrementally to isolate issues:
from tif1.schedule_schema import validate_schedule_payload

# Start with minimal structure
payload = {"schema_version": 1, "years": {}}
validate_schedule_payload(payload)  # Should pass

# Add one year
payload["years"]["2021"] = {"events": [], "sessions": {}}
validate_schedule_payload(payload)  # Should pass

# Add one event
payload["years"]["2021"]["events"] = ["Bahrain Grand Prix"]
# Don't validate yet - we know this will fail (missing sessions)

# Add corresponding sessions
payload["years"]["2021"]["sessions"]["Bahrain Grand Prix"] = ["Race"]
validate_schedule_payload(payload)  # Should pass

Strategy 2: Payload Inspection

Inspect the payload structure before validation:
import json
from tif1.schedule_schema import validate_schedule_payload

def debug_validate(payload):
    """Validate with detailed debugging output."""
    print("=== Payload Structure ===")
    print(json.dumps(payload, indent=2))

    print("\n=== Validation Checks ===")

    # Check 1: Type
    print(f"1. Payload type: {type(payload).__name__}")
    if not isinstance(payload, dict):
        print("   ✗ FAIL: Must be dict")
        return
    print("   ✓ PASS")

    # Check 2: Schema version
    version = payload.get("schema_version")
    print(f"2. Schema version: {version}")
    if version != 1:
        print(f"   ✗ FAIL: Must be 1, got {version}")
        return
    print("   ✓ PASS")

    # Check 3: Years
    years = payload.get("years")
    print(f"3. Years type: {type(years).__name__}")
    if not isinstance(years, dict):
        print("   ✗ FAIL: Must be dict")
        return
    print(f"   ✓ PASS ({len(years)} years)")

    # Check 4: Each year
    for year_key, year_data in years.items():
        print(f"\n4. Year '{year_key}':")
        print(f"   - Key format: {'✓' if year_key.isdigit() else '✗'}")
        print(f"   - Value type: {type(year_data).__name__}")

        if isinstance(year_data, dict):
            events = year_data.get("events", [])
            sessions = year_data.get("sessions", {})
            print(f"   - Events: {len(events)} items")
            print(f"   - Sessions: {len(sessions)} mappings")

            # Check event-session consistency
            for event in events:
                has_sessions = event in sessions
                print(f"     • '{event}': {'✓' if has_sessions else '✗ MISSING SESSIONS'}")

    print("\n=== Running Validation ===")
    try:
        validate_schedule_payload(payload)
        print("✓ VALIDATION PASSED")
    except Exception as e:
        print(f"✗ VALIDATION FAILED: {e}")

Strategy 3: Diff Against Known-Good Payload

Compare a failing payload against a known-good example:
import json
from deepdiff import DeepDiff  # pip install deepdiff

def compare_payloads(test_payload, reference_payload):
    """Compare test payload structure against reference."""
    diff = DeepDiff(
        reference_payload,
        test_payload,
        ignore_order=True,
        view='tree'
    )

    if diff:
        print("=== Structural Differences ===")
        print(json.dumps(diff.to_dict(), indent=2))
    else:
        print("✓ Structures match")

Common Error Scenarios and Solutions

ScenarioErrorRoot CauseSolution
Loading JSON from fileSchedule payload must be an objectJSON file contains array at rootWrap array in {"schema_version": 1, "years": {...}}
Converting from DataFrameInvalid year key: '2021'Year column is numericConvert to string: str(year)
Merging schedulesInvalid session list for year=X event='Y'Event in one schedule missing sessionsEnsure all events have session mappings
Custom data sourceUnsupported schedule schema version: NoneMissing schema_version fieldAdd "schema_version": 1 to payload
API response parsingYear payload must be object for year=XYear data is list instead of dictTransform API response structure
Debugging Tip: When validation fails, print the exact payload structure using json.dumps(payload, indent=2) to visually inspect the data hierarchy and identify structural issues.

Integration with tif1

How Validation Fits into tif1’s Architecture

The schedule validation system is a foundational component that enables reliable operation of all schedule-related APIs in tif1. Here’s how it integrates:

Internal Usage Flow

# Internal flow (simplified)
def _load_schedule_payload():
    """Internal function that loads and validates schedule data."""
    # 1. Load vendored schedule files
    vendored_years = _load_vendored_f1schedule_years()

    # 2. Construct payload
    payload = {
        "schema_version": 1,
        "years": vendored_years
    }

    # 3. Validate before caching
    validated = validate_schedule_payload(payload)

    # 4. Cache and return
    return validated

Public APIs That Use Validation

All schedule-related public APIs depend on validated schedule data:

get_events(year)

Returns a list of event names for a given year.
import tif1

# Internally validates schedule before returning events
events = tif1.get_events(2021)
print(events)
# Output: ['Bahrain Grand Prix', 'Emilia Romagna Grand Prix', ...]
Internal Flow:
get_events(2021)
  → _load_schedule_payload()  # Validates here
    → validate_schedule_payload(payload)
  → Extract events for 2021
  → Return event list

get_sessions(year, event)

Returns a list of session names for a specific event.
import tif1

# Internally validates schedule before returning sessions
sessions = tif1.get_sessions(2021, "Belgian Grand Prix")
print(sessions)
# Output: ['Practice 1', 'Practice 2', 'Practice 3', 'Qualifying', 'Race']
Internal Flow:
get_sessions(2021, "Belgian Grand Prix")
  → _load_schedule_payload()  # Validates here
    → validate_schedule_payload(payload)
  → Extract sessions for event
  → Return session list

get_event_schedule(year)

Returns a pandas DataFrame with the complete event schedule for a year.
import tif1

# Internally validates schedule before building DataFrame
schedule = tif1.get_event_schedule(2021)
print(schedule[['RoundNumber', 'EventName', 'Location']])
Internal Flow:
get_event_schedule(2021)
  → _load_schedule_payload()  # Validates here
    → validate_schedule_payload(payload)
  → Build Event objects with metadata
  → Construct EventSchedule DataFrame
  → Return schedule

get_event(year, identifier)

Returns an Event object for a specific event.
import tif1

# Internally validates schedule before creating Event
event = tif1.get_event(2021, "Belgian Grand Prix")
print(f"{event['EventName']} at {event['Location']}")
# Output: Belgian Grand Prix at Spa-Francorchamps
Internal Flow:
get_event(2021, "Belgian Grand Prix")
  → _load_schedule_payload()  # Validates here
    → validate_schedule_payload(payload)
  → Find event by name (with fuzzy matching)
  → Create Event object with metadata
  → Return event

Data Source Priority

tif1 uses a fallback system for schedule data:
┌─────────────────────────────────────────────────────────────┐
│ 1. Vendored JSON Files (Primary Source)                     │
│    Location: src/tif1/data/schedules/f1schedule/            │
│    Coverage: Years bundled with library (typically 2018+)   │
│    Validation: Runs once per Python session                 │
└─────────────────────────────────────────────────────────────┘
                          ↓ (if year not found)
┌─────────────────────────────────────────────────────────────┐
│ 2. CDN Fallback (Secondary Source)                          │
│    URL: jsdelivr.net/gh/theOehrly/f1schedule@master/        │
│    Coverage: All years available in f1schedule repo         │
│    Validation: Runs for each fetched year                   │
│    Caching: Cached after successful fetch                   │
└─────────────────────────────────────────────────────────────┘
Both sources go through the same validation pipeline, ensuring consistent data quality.

Caching Strategy

Validation results are cached to minimize overhead:
from functools import lru_cache

@lru_cache(maxsize=1)
def _load_schedule_payload():
    """Load and validate schedule payload (cached)."""
    # Validation happens here, but only once per Python session
    vendored_years = _load_vendored_f1schedule_years()
    payload = {"schema_version": 1, "years": vendored_years}
    return validate_schedule_payload(payload)

@lru_cache(maxsize=16)
def _load_f1schedule_year_from_cdn(year):
    """Load and validate year from CDN (cached per year)."""
    # Validation happens here, but only once per year per session
    raw_data = fetch_from_cdn(year)
    converted = _convert_f1schedule_year(raw_data, year)
    # Validation is implicit in the conversion process
    return converted
Cache Characteristics:
  • Vendored Data: Validated once per Python session, cached indefinitely
  • CDN Data: Validated once per year per session, cached with LRU eviction (16 years max)
  • Performance Impact: First call ~1ms, subsequent calls ~0.001ms (cache hit)

Custom Schedule Data Integration

You can integrate custom schedule data with tif1’s validation system:

Example: Loading Custom Schedule File

import json
from tif1.schedule_schema import validate_schedule_payload
from tif1.exceptions import InvalidDataError

def load_custom_schedule(file_path):
    """
    Load a custom schedule file and validate it.

    Args:
        file_path: Path to JSON file containing schedule data

    Returns:
        Validated schedule payload

    Raises:
        InvalidDataError: If schedule data is invalid
        FileNotFoundError: If file doesn't exist
        json.JSONDecodeError: If file is not valid JSON
    """
    # Load JSON file
    with open(file_path, 'r', encoding='utf-8') as f:
        payload = json.load(f)

    # Validate structure
    validated = validate_schedule_payload(payload)

    print(f"✓ Loaded and validated schedule from {file_path}")
    print(f"  Years: {list(validated['years'].keys())}")

    return validated

# Usage
try:
    custom_schedule = load_custom_schedule("my_schedule.json")
    # Use validated schedule data
    for year in custom_schedule['years']:
        events = custom_schedule['years'][year]['events']
        print(f"{year}: {len(events)} events")
except InvalidDataError as e:
    print(f"Invalid schedule file: {e.message}")
except FileNotFoundError:
    print("Schedule file not found")
except json.JSONDecodeError as e:
    print(f"Invalid JSON: {e}")

Example: Building Schedule Programmatically

from tif1.schedule_schema import validate_schedule_payload

def build_custom_schedule(years_data):
    """
    Build a schedule payload from structured data.

    Args:
        years_data: Dict mapping years to (events, sessions) tuples

    Returns:
        Validated schedule payload
    """
    payload = {
        "schema_version": 1,
        "years": {}
    }

    for year, (events, sessions) in years_data.items():
        payload["years"][str(year)] = {
            "events": events,
            "sessions": sessions
        }

    # Validate before returning
    return validate_schedule_payload(payload)

# Usage
years_data = {
    2021: (
        ["Bahrain Grand Prix", "Belgian Grand Prix"],
        {
            "Bahrain Grand Prix": ["Practice 1", "Qualifying", "Race"],
            "Belgian Grand Prix": ["Practice 1", "Qualifying", "Race"]
        }
    ),
    2022: (
        ["Bahrain Grand Prix", "Saudi Arabian Grand Prix"],
        {
            "Bahrain Grand Prix": ["Practice 1", "Qualifying", "Race"],
            "Saudi Arabian Grand Prix": ["Practice 1", "Qualifying", "Race"]
        }
    )
}

schedule = build_custom_schedule(years_data)
print(f"Built schedule with {len(schedule['years'])} years")

Example: Extending Vendored Schedule

from tif1.schedule_schema import validate_schedule_payload
from tif1.events import _load_schedule_payload

def extend_schedule_with_custom_year(year, events, sessions):
    """
    Extend tif1's vendored schedule with a custom year.

    Args:
        year: Year to add (int)
        events: List of event names
        sessions: Dict mapping event names to session lists

    Returns:
        Extended and validated schedule payload
    """
    # Load existing schedule
    base_schedule = _load_schedule_payload()

    # Add custom year
    base_schedule["years"][str(year)] = {
        "events": events,
        "sessions": sessions
    }

    # Re-validate with new data
    return validate_schedule_payload(base_schedule)

# Usage
extended = extend_schedule_with_custom_year(
    year=2026,
    events=["Bahrain Grand Prix", "Saudi Arabian Grand Prix"],
    sessions={
        "Bahrain Grand Prix": ["Practice 1", "Qualifying", "Race"],
        "Saudi Arabian Grand Prix": ["Practice 1", "Qualifying", "Race"]
    }
)

print(f"Extended schedule now includes: {list(extended['years'].keys())}")

Testing with Validation

When writing tests for schedule-related code, use validation to ensure test data quality:
import pytest
from tif1.schedule_schema import validate_schedule_payload
from tif1.exceptions import InvalidDataError

@pytest.fixture
def valid_schedule_payload():
    """Fixture providing a valid schedule payload for testing."""
    payload = {
        "schema_version": 1,
        "years": {
            "2021": {
                "events": ["Test Grand Prix"],
                "sessions": {
                    "Test Grand Prix": ["Practice 1", "Qualifying", "Race"]
                }
            }
        }
    }
    # Validate fixture data
    return validate_schedule_payload(payload)

def test_schedule_processing(valid_schedule_payload):
    """Test that uses validated schedule data."""
    # Test code here knows the payload is valid
    assert "2021" in valid_schedule_payload["years"]
    assert len(valid_schedule_payload["years"]["2021"]["events"]) == 1

def test_invalid_schedule_handling():
    """Test error handling for invalid schedules."""
    invalid_payload = {
        "schema_version": 1,
        "years": {
            "2021": {
                "events": ["Test Grand Prix"],
                "sessions": {}  # Missing sessions!
            }
        }
    }

    with pytest.raises(InvalidDataError) as exc_info:
        validate_schedule_payload(invalid_payload)

    assert "Invalid session list" in str(exc_info.value)

Advanced Topics

Raw f1schedule Format vs Internal Format

tif1 uses two different data formats internally:

Raw f1schedule Format (Columnar)

The vendored JSON files use a columnar format similar to pandas DataFrames:
{
  "round_number": {
    "0": 0,
    "1": 1,
    "2": 2
  },
  "event_name": {
    "0": "Pre-Season Test",
    "1": "Bahrain Grand Prix",
    "2": "Emilia Romagna Grand Prix"
  },
  "country": {
    "0": "Bahrain",
    "1": "Bahrain",
    "2": "Italy"
  },
  "session1": {
    "0": "Practice 1",
    "1": "Practice 1",
    "2": "Practice 1"
  },
  "session1_date": {
    "0": "2021-03-12T10:00:00",
    "1": "2021-03-26T14:30:00",
    "2": "2021-04-16T11:00:00"
  }
  // ... more columns
}
Characteristics:
  • Structure: Column-oriented (each field is a dictionary mapping indices to values)
  • Efficiency: Compact storage, easy to convert to/from pandas DataFrames
  • Query Pattern: Requires iteration to find specific events
  • Source: f1schedule repository (https://github.com/theOehrly/f1schedule)

Internal Format (Event-Centric)

After conversion and validation, tif1 uses an event-centric format:
{
  "schema_version": 1,
  "years": {
    "2021": {
      "events": [
        "Pre-Season Test",
        "Bahrain Grand Prix",
        "Emilia Romagna Grand Prix"
      ],
      "sessions": {
        "Pre-Season Test": ["Practice 1", "Practice 2", "Practice 3"],
        "Bahrain Grand Prix": ["Practice 1", "Practice 2", "Practice 3", "Qualifying", "Race"],
        "Emilia Romagna Grand Prix": ["Practice 1", "Practice 2", "Practice 3", "Qualifying", "Race"]
      },
      "metadata": {
        "Bahrain Grand Prix": {
          "RoundNumber": 1,
          "Country": "Bahrain",
          "Session1Date": "2021-03-26T14:30:00"
          // ... more metadata
        }
      }
    }
  }
}
Characteristics:
  • Structure: Event-oriented (each event is a top-level entity)
  • Efficiency: Fast event and session lookups (O(1) dictionary access)
  • Query Pattern: Direct access by event name
  • Source: Converted from raw format by _convert_f1schedule_year()

Conversion Process

The conversion from raw to internal format happens in _convert_f1schedule_year():
def _convert_f1schedule_year(raw_year: dict, year: int) -> dict:
    """
    Convert raw f1schedule columnar format to internal event-centric format.

    Process:
    1. Extract column data (event_name, round_number, sessions, etc.)
    2. Iterate through indices to build event list
    3. Extract sessions for each event
    4. Build metadata dictionary
    5. Sort events (race events by round, testing by date)
    6. Return structured payload
    """
    # Implementation details in events.py
Why Two Formats?
  • Raw Format: Optimized for storage and distribution (smaller file size, easier to maintain)
  • Internal Format: Optimized for runtime queries (faster lookups, better API ergonomics)

Performance Optimization Techniques

Technique 1: Lazy Loading

Schedule data is loaded only when first accessed:
from functools import lru_cache

@lru_cache(maxsize=1)
def _load_schedule_payload():
    """Load schedule only once per session."""
    # Expensive operations happen here
    vendored_years = _load_vendored_f1schedule_years()
    payload = {"schema_version": 1, "years": vendored_years}
    return validate_schedule_payload(payload)

# First call: loads and validates (~10-20ms)
schedule1 = _load_schedule_payload()

# Subsequent calls: returns cached result (~0.001ms)
schedule2 = _load_schedule_payload()

Technique 2: Immutable Caching

Event and session lists are cached as immutable tuples:
@lru_cache(maxsize=16)
def _get_events_cached(year: int) -> tuple[str, ...]:
    """Cache events as immutable tuple for hashability."""
    return tuple(_build_events_for_year(year))

@lru_cache(maxsize=128)
def _get_sessions_cached(year: int, event: str) -> tuple[str, ...]:
    """Cache sessions as immutable tuple."""
    return tuple(_build_sessions_for_event(year, event))
Benefits:
  • Tuples are hashable (can be used as cache keys)
  • Immutability prevents accidental modification
  • Smaller memory footprint than lists

Technique 3: Validation Short-Circuiting

Validation stops at the first error:
def validate_schedule_payload(payload):
    # Check 1: Type
    if not isinstance(payload, dict):
        raise InvalidDataError(...)  # Stop here

    # Check 2: Schema version
    if payload.get("schema_version") != 1:
        raise InvalidDataError(...)  # Stop here

    # ... more checks
Benefits:
  • Faster failure for invalid data
  • Reduces unnecessary computation
  • Provides immediate feedback

Technique 4: In-Place Validation

Validation doesn’t copy data:
def validate_schedule_payload(payload):
    # Validates structure without creating copies
    # Returns the same object if valid
    return payload
Benefits:
  • Zero memory overhead
  • Faster validation (no allocation/copying)
  • Suitable for large schedules

Extending the Validation System

Adding Custom Validation Rules

You can wrap the built-in validation with additional checks:
from tif1.schedule_schema import validate_schedule_payload
from tif1.exceptions import InvalidDataError

def validate_schedule_with_business_rules(payload):
    """
    Validate schedule with additional business logic.
    """
    # First, run standard validation
    validated = validate_schedule_payload(payload)

    # Then, add custom rules
    for year_str, year_data in validated["years"].items():
        year = int(year_str)

        # Rule 1: Modern seasons must have at least 15 events
        if year >= 2020:
            events = year_data.get("events", [])
            if len(events) < 15:
                raise InvalidDataError(
                    reason=f"Year {year} has only {len(events)} events (minimum 15 required)"
                )

        # Rule 2: All events must have at least 3 sessions
        sessions = year_data.get("sessions", {})
        for event, session_list in sessions.items():
            if len(session_list) < 3:
                raise InvalidDataError(
                    reason=f"Event '{event}' in {year} has only {len(session_list)} sessions"
                )

        # Rule 3: Race session must exist for all events
        for event, session_list in sessions.items():
            if "Race" not in session_list:
                raise InvalidDataError(
                    reason=f"Event '{event}' in {year} missing 'Race' session"
                )

    return validated

Creating Validation Decorators

Wrap functions with validation:
from functools import wraps
from tif1.schedule_schema import validate_schedule_payload

def requires_valid_schedule(func):
    """Decorator that validates schedule payload before function execution."""
    @wraps(func)
    def wrapper(payload, *args, **kwargs):
        # Validate payload
        validated = validate_schedule_payload(payload)
        # Call original function with validated payload
        return func(validated, *args, **kwargs)
    return wrapper

@requires_valid_schedule
def process_schedule(payload):
    """Process schedule data (payload is guaranteed valid)."""
    for year in payload["years"]:
        print(f"Processing {year}...")
        # Process with confidence that structure is valid

Building Validation Pipelines

Chain multiple validation steps:
from tif1.schedule_schema import validate_schedule_payload
from tif1.exceptions import InvalidDataError

class ScheduleValidationPipeline:
    """Pipeline for multi-stage schedule validation."""

    def __init__(self):
        self.validators = []

    def add_validator(self, validator_func, name=None):
        """Add a validation function to the pipeline."""
        self.validators.append((name or validator_func.__name__, validator_func))
        return self

    def validate(self, payload):
        """Run all validators in sequence."""
        current = payload

        for name, validator in self.validators:
            try:
                current = validator(current)
                print(f"✓ {name} passed")
            except InvalidDataError as e:
                print(f"✗ {name} failed: {e.message}")
                raise

        return current

# Usage
pipeline = ScheduleValidationPipeline()
pipeline.add_validator(validate_schedule_payload, "Schema Validation")
pipeline.add_validator(validate_schedule_with_business_rules, "Business Rules")

try:
    validated = pipeline.validate(my_payload)
    print("✓ All validations passed")
except InvalidDataError as e:
    print(f"✗ Validation failed: {e.message}")

Schema Evolution and Migration

Handling Future Schema Versions

When schema version 2 is introduced, you might need migration logic:
from tif1.schedule_schema import validate_schedule_payload
from tif1.exceptions import InvalidDataError

def migrate_schedule_to_v1(payload):
    """
    Migrate schedule payload to schema version 1.

    Handles:
    - Version 0 (hypothetical legacy format)
    - Version 1 (current format)
    - Version 2 (future format - downgrades to v1)
    """
    version = payload.get("schema_version")

    if version == 1:
        # Already v1, just validate
        return validate_schedule_payload(payload)

    elif version == 0:
        # Migrate from v0 to v1
        migrated = {
            "schema_version": 1,
            "years": {}
        }

        # Migration logic here
        for year, data in payload.get("schedule", {}).items():
            migrated["years"][str(year)] = {
                "events": data.get("event_list", []),
                "sessions": data.get("session_map", {})
            }

        return validate_schedule_payload(migrated)

    elif version == 2:
        # Downgrade from v2 to v1 (strip v2-specific fields)
        downgraded = {
            "schema_version": 1,
            "years": {}
        }

        for year, data in payload.get("years", {}).items():
            downgraded["years"][year] = {
                "events": data.get("events", []),
                "sessions": data.get("sessions", {}),
                # Omit v2-specific fields like "circuits", "weather", etc.
            }

        return validate_schedule_payload(downgraded)

    else:
        raise InvalidDataError(
            reason=f"Cannot migrate from unsupported schema version: {version}"
        )

Troubleshooting Guide

Issue: Validation Passes But API Fails

Symptom: validate_schedule_payload() succeeds, but get_sessions() returns empty list. Cause: Metadata might be missing or malformed (not validated by schema validator). Solution:
# Check metadata separately
payload = validate_schedule_payload(payload)
year_data = payload["years"]["2021"]

if "metadata" in year_data:
    for event in year_data["events"]:
        if event not in year_data["metadata"]:
            print(f"Warning: Missing metadata for '{event}'")

Issue: Performance Degradation

Symptom: Validation becomes slow with large schedules. Cause: Validation is O(n×m) where n=years, m=events per year. Solution:
# Profile validation
import time

start = time.perf_counter()
validated = validate_schedule_payload(payload)
elapsed = time.perf_counter() - start

print(f"Validation took {elapsed*1000:.2f}ms")

# If slow, check payload size
total_events = sum(
    len(year_data.get("events", []))
    for year_data in payload["years"].values()
)
print(f"Total events: {total_events}")

Issue: Cryptic Error Messages

Symptom: Error message doesn’t clearly indicate the problem. Cause: Complex nested structure makes errors hard to pinpoint. Solution:
# Use debug validation function (from earlier examples)
debug_validate(payload)

# Or add try-except around specific sections
try:
    validate_schedule_payload(payload)
except InvalidDataError as e:
    print(f"Error: {e.message}")
    print(f"Context: {e.context}")

    # Inspect payload at failure point
    import json
    print("\nPayload structure:")
    print(json.dumps(payload, indent=2, default=str))

Complete Working Examples

Example 1: Basic Schedule Validation

"""
Basic example: Validate a simple schedule payload.
"""
from tif1.schedule_schema import validate_schedule_payload
from tif1.exceptions import InvalidDataError

# Create a minimal valid schedule
schedule = {
    "schema_version": 1,
    "years": {
        "2021": {
            "events": [
                "Bahrain Grand Prix",
                "Belgian Grand Prix",
                "Abu Dhabi Grand Prix"
            ],
            "sessions": {
                "Bahrain Grand Prix": [
                    "Practice 1", "Practice 2", "Practice 3",
                    "Qualifying", "Race"
                ],
                "Belgian Grand Prix": [
                    "Practice 1", "Practice 2", "Practice 3",
                    "Qualifying", "Race"
                ],
                "Abu Dhabi Grand Prix": [
                    "Practice 1", "Practice 2", "Practice 3",
                    "Qualifying", "Race"
                ]
            }
        }
    }
}

try:
    validated = validate_schedule_payload(schedule)
    print("✓ Schedule validation successful!")
    print(f"  Years: {list(validated['years'].keys())}")
    print(f"  Events in 2021: {len(validated['years']['2021']['events'])}")

    for event in validated['years']['2021']['events']:
        sessions = validated['years']['2021']['sessions'][event]
        print(f"  • {event}: {len(sessions)} sessions")

except InvalidDataError as e:
    print(f"✗ Validation failed: {e.message}")
    print(f"  Reason: {e.context.get('reason')}")

Example 2: Loading and Validating from JSON File

"""
Load schedule data from a JSON file and validate it.
"""
import json
from pathlib import Path
from tif1.schedule_schema import validate_schedule_payload
from tif1.exceptions import InvalidDataError

def load_schedule_file(file_path: str | Path) -> dict:
    """
    Load and validate a schedule JSON file.

    Args:
        file_path: Path to the JSON file

    Returns:
        Validated schedule payload

    Raises:
        FileNotFoundError: If file doesn't exist
        json.JSONDecodeError: If file contains invalid JSON
        InvalidDataError: If schedule structure is invalid
    """
    file_path = Path(file_path)

    # Check file exists
    if not file_path.exists():
        raise FileNotFoundError(f"Schedule file not found: {file_path}")

    # Load JSON
    print(f"Loading schedule from {file_path}...")
    with open(file_path, 'r', encoding='utf-8') as f:
        payload = json.load(f)

    # Validate
    print("Validating schedule structure...")
    validated = validate_schedule_payload(payload)

    # Report success
    years = list(validated['years'].keys())
    total_events = sum(
        len(validated['years'][year]['events'])
        for year in years
    )

    print(f"✓ Schedule loaded successfully")
    print(f"  Years: {', '.join(years)}")
    print(f"  Total events: {total_events}")

    return validated

# Usage
try:
    schedule = load_schedule_file("my_schedule.json")

    # Use the validated schedule
    for year in schedule['years']:
        events = schedule['years'][year]['events']
        print(f"\n{year} Season ({len(events)} events):")
        for event in events[:3]:  # Show first 3
            print(f"  • {event}")
        if len(events) > 3:
            print(f"  ... and {len(events) - 3} more")

except FileNotFoundError as e:
    print(f"✗ Error: {e}")
except json.JSONDecodeError as e:
    print(f"✗ Invalid JSON: {e}")
except InvalidDataError as e:
    print(f"✗ Invalid schedule structure: {e.message}")

Example 3: Building Schedule Programmatically

"""
Build a schedule payload programmatically with validation.
"""
from tif1.schedule_schema import validate_schedule_payload
from tif1.exceptions import InvalidDataError

class ScheduleBuilder:
    """Helper class for building valid schedule payloads."""

    def __init__(self):
        self.payload = {
            "schema_version": 1,
            "years": {}
        }

    def add_year(self, year: int):
        """Add a year to the schedule."""
        self.payload["years"][str(year)] = {
            "events": [],
            "sessions": {},
            "metadata": {}
        }
        return self

    def add_event(self, year: int, event_name: str, sessions: list[str],
                  metadata: dict | None = None):
        """Add an event to a specific year."""
        year_str = str(year)

        if year_str not in self.payload["years"]:
            self.add_year(year)

        # Add to events list
        self.payload["years"][year_str]["events"].append(event_name)

        # Add sessions
        self.payload["years"][year_str]["sessions"][event_name] = sessions

        # Add metadata if provided
        if metadata:
            self.payload["years"][year_str]["metadata"][event_name] = metadata

        return self

    def build(self):
        """Build and validate the schedule."""
        return validate_schedule_payload(self.payload)

# Usage
try:
    builder = ScheduleBuilder()

    # Add 2021 season
    builder.add_event(
        year=2021,
        event_name="Bahrain Grand Prix",
        sessions=["Practice 1", "Practice 2", "Practice 3", "Qualifying", "Race"],
        metadata={
            "RoundNumber": 1,
            "Location": "Sakhir",
            "Country": "Bahrain"
        }
    )

    builder.add_event(
        year=2021,
        event_name="Belgian Grand Prix",
        sessions=["Practice 1", "Practice 2", "Practice 3", "Qualifying", "Race"],
        metadata={
            "RoundNumber": 12,
            "Location": "Spa-Francorchamps",
            "Country": "Belgium"
        }
    )

    # Add 2022 season
    builder.add_event(
        year=2022,
        event_name="Bahrain Grand Prix",
        sessions=["Practice 1", "Practice 2", "Practice 3", "Qualifying", "Race"],
        metadata={
            "RoundNumber": 1,
            "Location": "Sakhir",
            "Country": "Bahrain"
        }
    )

    # Build and validate
    schedule = builder.build()

    print("✓ Schedule built successfully!")
    for year in schedule['years']:
        events = schedule['years'][year]['events']
        print(f"  {year}: {len(events)} events")

except InvalidDataError as e:
    print(f"✗ Failed to build schedule: {e.message}")

Example 4: Validating Sprint Weekend Format

"""
Example showing validation of sprint weekend schedules.
"""
from tif1.schedule_schema import validate_schedule_payload

# 2021 Sprint Weekend (British GP)
sprint_2021 = {
    "schema_version": 1,
    "years": {
        "2021": {
            "events": ["British Grand Prix"],
            "sessions": {
                "British Grand Prix": [
                    "Practice 1",      # Friday
                    "Qualifying",      # Friday (for Sunday grid)
                    "Practice 2",      # Saturday
                    "Sprint",          # Saturday (sets Sunday grid)
                    "Race"             # Sunday
                ]
            },
            "metadata": {
                "British Grand Prix": {
                    "RoundNumber": 10,
                    "EventFormat": "sprint",
                    "Location": "Silverstone",
                    "Country": "Great Britain"
                }
            }
        }
    }
}

# 2023 Sprint Weekend (with Sprint Shootout)
sprint_2023 = {
    "schema_version": 1,
    "years": {
        "2023": {
            "events": ["Azerbaijan Grand Prix"],
            "sessions": {
                "Azerbaijan Grand Prix": [
                    "Practice 1",         # Friday
                    "Qualifying",         # Friday (for Sunday grid)
                    "Sprint Shootout",    # Saturday (for Sprint grid)
                    "Sprint",             # Saturday
                    "Race"                # Sunday
                ]
            },
            "metadata": {
                "Azerbaijan Grand Prix": {
                    "RoundNumber": 4,
                    "EventFormat": "sprint",
                    "Location": "Baku",
                    "Country": "Azerbaijan"
                }
            }
        }
    }
}

# Validate both formats
for name, schedule in [("2021 Sprint", sprint_2021), ("2023 Sprint", sprint_2023)]:
    try:
        validated = validate_schedule_payload(schedule)
        year = list(validated['years'].keys())[0]
        event = validated['years'][year]['events'][0]
        sessions = validated['years'][year]['sessions'][event]

        print(f"✓ {name} format validated")
        print(f"  Event: {event}")
        print(f"  Sessions: {', '.join(sessions)}")
        print()
    except Exception as e:
        print(f"✗ {name} validation failed: {e}")

Example 5: Integration with tif1 APIs

"""
Complete example showing how validation integrates with tif1's public APIs.
"""
import tif1
from tif1.schedule_schema import validate_schedule_payload
from tif1.exceptions import InvalidDataError

# The validation happens automatically when using tif1 APIs
print("=== Using tif1 Public APIs ===\n")

# Get events for 2021 (internally validates schedule)
events = tif1.get_events(2021)
print(f"2021 Events ({len(events)} total):")
for event in events[:5]:
    print(f"  • {event}")
print(f"  ... and {len(events) - 5} more\n")

# Get sessions for a specific event (internally validates schedule)
sessions = tif1.get_sessions(2021, "Belgian Grand Prix")
print(f"Belgian Grand Prix 2021 Sessions:")
for session in sessions:
    print(f"  • {session}")
print()

# Get full event schedule (internally validates schedule)
schedule = tif1.get_event_schedule(2021)
print(f"Full 2021 Schedule:")
print(f"  Type: {type(schedule).__name__}")
print(f"  Shape: {schedule.shape}")
print(f"  Columns: {list(schedule.columns[:5])}...")
print()

# Get specific event (internally validates schedule)
event = tif1.get_event(2021, "Belgian Grand Prix")
print(f"Belgian Grand Prix Event:")
print(f"  Round: {event['RoundNumber']}")
print(f"  Location: {event['Location']}")
print(f"  Country: {event['Country']}")
print(f"  Format: {event['EventFormat']}")
print()

# Manual validation (for custom data)
print("=== Manual Validation ===\n")

custom_schedule = {
    "schema_version": 1,
    "years": {
        "2026": {
            "events": ["Test Grand Prix"],
            "sessions": {
                "Test Grand Prix": ["Practice 1", "Qualifying", "Race"]
            }
        }
    }
}

try:
    validated = validate_schedule_payload(custom_schedule)
    print("✓ Custom schedule validated successfully")
    print(f"  Years: {list(validated['years'].keys())}")
except InvalidDataError as e:
    print(f"✗ Validation failed: {e.message}")

Example 6: Error Handling and Recovery

"""
Comprehensive error handling example.
"""
from tif1.schedule_schema import validate_schedule_payload
from tif1.exceptions import InvalidDataError

def validate_with_detailed_error_handling(payload):
    """
    Validate schedule with comprehensive error handling and reporting.
    """
    try:
        validated = validate_schedule_payload(payload)

        # Success - report details
        years = list(validated['years'].keys())
        total_events = sum(
            len(validated['years'][year]['events'])
            for year in years
        )
        total_sessions = sum(
            len(session_list)
            for year in years
            for session_list in validated['years'][year]['sessions'].values()
        )

        print("✓ Validation successful!")
        print(f"  Years: {len(years)} ({', '.join(years)})")
        print(f"  Total events: {total_events}")
        print(f"  Total sessions: {total_sessions}")

        return validated

    except InvalidDataError as e:
        # Detailed error analysis
        reason = e.context.get('reason', '')

        print("✗ Validation failed!")
        print(f"  Error: {e.message}")
        print(f"  Reason: {reason}")

        # Provide specific guidance based on error type
        if "schema version" in reason.lower():
            print("\n  💡 Fix: Set 'schema_version' to 1")
            print("     Example: {'schema_version': 1, 'years': {...}}")

        elif "year key" in reason.lower():
            print("\n  💡 Fix: Use numeric string for year keys")
            print("     Example: '2021' not 2021 or 'twenty-one'")

        elif "events list" in reason.lower():
            print("\n  💡 Fix: Ensure 'events' is a list of non-empty strings")
            print("     Example: 'events': ['Bahrain Grand Prix', 'Belgian Grand Prix']")

        elif "session list" in reason.lower():
            print("\n  💡 Fix: Ensure every event has a session list")
            print("     Example: 'sessions': {'Event Name': ['Practice 1', 'Race']}")

        else:
            print("\n  💡 Check the payload structure against the schema")

        return None

# Test cases
test_cases = [
    # Valid payload
    {
        "name": "Valid Schedule",
        "payload": {
            "schema_version": 1,
            "years": {
                "2021": {
                    "events": ["Bahrain Grand Prix"],
                    "sessions": {
                        "Bahrain Grand Prix": ["Practice 1", "Qualifying", "Race"]
                    }
                }
            }
        }
    },
    # Invalid: wrong schema version
    {
        "name": "Wrong Schema Version",
        "payload": {
            "schema_version": 2,
            "years": {}
        }
    },
    # Invalid: missing sessions
    {
        "name": "Missing Sessions",
        "payload": {
            "schema_version": 1,
            "years": {
                "2021": {
                    "events": ["Bahrain Grand Prix"],
                    "sessions": {}
                }
            }
        }
    }
]

# Run test cases
for test in test_cases:
    print(f"\n{'='*60}")
    print(f"Test: {test['name']}")
    print('='*60)
    validate_with_detailed_error_handling(test['payload'])

Summary

The schedule_schema module provides the foundational validation layer for tif1’s schedule data system. Key takeaways:

Core Concepts

  • Single Validation Function: validate_schedule_payload() is the only public API
  • Schema Version 1: Currently the only supported version
  • Event-Centric Structure: Internal format optimized for fast event/session lookups
  • Fail-Fast Validation: Stops at first error with detailed error messages
  • Performance Optimized: Typically <1ms validation time, cached results

When to Use

  • Automatic: All tif1 schedule APIs use validation internally
  • Manual: When working with custom schedule data or building data pipelines
  • Testing: To ensure test fixtures have valid structure
  • Debugging: To diagnose schedule data issues

Best Practices

  1. Validate Early: Run validation immediately after loading or constructing schedule data
  2. Handle Errors: Always catch InvalidDataError and provide user-friendly messages
  3. Preserve Context: When wrapping validation, preserve error context for debugging
  4. Build Incrementally: For complex payloads, validate at each construction stage
  5. Use Caching: Leverage tif1’s built-in caching to minimize validation overhead

Common Patterns

# Pattern 1: Load and validate from file
with open("schedule.json") as f:
    payload = json.load(f)
validated = validate_schedule_payload(payload)

# Pattern 2: Build programmatically
payload = {
    "schema_version": 1,
    "years": {
        "2021": {
            "events": ["Event 1"],
            "sessions": {"Event 1": ["Session 1"]}
        }
    }
}
validated = validate_schedule_payload(payload)

# Pattern 3: Error handling
try:
    validated = validate_schedule_payload(payload)
except InvalidDataError as e:
    print(f"Validation failed: {e.context.get('reason')}")
For more information on working with schedule data in tif1:

Events & Schedule API

High-level APIs for accessing event and session information

Core Session API

Loading and working with session data

Exception Handling

Complete exception hierarchy and error handling patterns

Data Flow Concepts

Understanding tif1’s data pipeline architecture

Quick Reference

Validation Rules Summary

LevelWhat’s CheckedError Pattern
Top-LevelPayload is dict, has schema_version=1, has years dictSchedule payload must be an object
YearYear keys are numeric strings, values are dictsInvalid year key: {key}
EventsEach year has events list with non-empty stringsInvalid events list for year={year}
SessionsEach year has sessions dictInvalid sessions map for year={year}
MappingEach event has session list with non-empty stringsInvalid session list for year={year} event={event}

Performance Benchmarks

OperationTimeNotes
First validation0.3-0.8msIncludes parsing and checking
Cached access~0.001msLRU cache hit
Multi-year (5 years)~0.6ms~110 events, ~550 sessions
Multi-year (10 years)~1.1ms~220 events, ~1100 sessions

Schema Structure Quick Reference

{
  "schema_version": 1,                    # Required: Must be 1
  "years": {                              # Required: Dict of years
    "YYYY": {                             # Year as string
      "events": ["Event 1", "Event 2"],   # Required: List of event names
      "sessions": {                       # Required: Event → sessions mapping
        "Event 1": ["Session 1", "..."],  # Required for each event
        "Event 2": ["Session 1", "..."]
      },
      "metadata": {                       # Optional: Event metadata
        "Event 1": {                      # Optional per event
          "RoundNumber": 1,
          "Location": "...",
          # ... more fields
        }
      }
    }
  }
}

Additional Resources

Community

  • Discussions: Ask questions about schedule validation on GitHub Discussions
  • Contributing: Contributions to improve validation are welcome

Version History

VersionChanges
v0.1.0Initial release with schema version 1 support
CurrentSchema version 1 remains the only supported version
Stay Updated: Watch the tif1 repository for announcements about new schema versions or validation enhancements.

Last updated: April 2026
Last modified on May 8, 2026