Airbyte source connector for extracting data from the Xero accounting API with support for 21 data streams and incremental sync capabilities
—
Quality
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Specialized utilities for handling Xero-specific data formats and custom record extraction. These components handle the conversion of Xero's .NET JSON date formats to standard ISO 8601 timestamps and provide custom record extraction with automatic field path resolution.
Utility class for parsing and converting Xero's .NET JSON date format to standard Python datetime objects with proper timezone handling.
from datetime import datetime
from typing import List, Union, Mapping, Any
from dataclasses import dataclass, InitVar
import requests
from airbyte_cdk.sources.declarative.extractors.record_extractor import RecordExtractor
from airbyte_cdk.sources.declarative.interpolation import InterpolatedString
from airbyte_cdk.sources.declarative.decoders.decoder import Decoder
from airbyte_cdk.sources.declarative.types import Config
class ParseDates:
"""
Static utility class for parsing Xero date formats.
Xero uses .NET JSON date strings in the format "/Date(timestamp±offset)/"
where timestamp is milliseconds since epoch and offset is timezone.
"""
@staticmethod
def parse_date(value):
"""
Parse a Xero date string into a Python datetime object.
Supports both .NET JSON format and standard ISO 8601 format:
- .NET format: "/Date(1419937200000+0000)/"
- ISO format: "2014-12-30T07:00:00Z"
Args:
value (str): Date string in Xero format or ISO 8601 format
Returns:
datetime or None: Parsed datetime with UTC timezone, or None if parsing fails
Examples:
>>> ParseDates.parse_date("/Date(1419937200000+0000)/")
datetime.datetime(2014, 12, 30, 7, 0, tzinfo=datetime.timezone.utc)
>>> ParseDates.parse_date("/Date(1580628711500+0300)/")
datetime.datetime(2020, 2, 2, 10, 31, 51, 500000, tzinfo=datetime.timezone.utc)
>>> ParseDates.parse_date("not a date")
None
"""
@staticmethod
def convert_dates(obj):
"""
Recursively convert all Xero date strings in a nested data structure.
Performs in-place conversion of date strings to ISO 8601 format.
Searches through dictionaries and lists recursively to find and
convert any date strings.
Args:
obj (dict or list): Data structure containing potential date strings
Modifies the object in-place
Side Effects:
- Converts .NET JSON dates to ISO 8601 strings
- Ensures all dates have UTC timezone information
- Preserves non-date data unchanged
Examples:
>>> data = {
... "UpdatedDate": "/Date(1419937200000+0000)/",
... "Amount": 100.50,
... "Items": [{"Date": "/Date(1580628711500+0300)/"}]
... }
>>> ParseDates.convert_dates(data)
>>> print(data)
{
"UpdatedDate": "2014-12-30T07:00:00+00:00",
"Amount": 100.50,
"Items": [{"Date": "2020-02-02T10:31:51+00:00"}]
}
"""Dataclass-based record extractor that extends Airbyte's RecordExtractor with automatic date conversion for Xero API responses.
@dataclass
class CustomExtractor(RecordExtractor):
"""
Custom record extractor for Xero API responses with date parsing.
Extracts records from HTTP responses using configurable field paths
and automatically converts Xero date formats to ISO 8601.
"""
field_path: List[Union[InterpolatedString, str]]
"""
Path to extract records from the response JSON.
Supports nested paths and wildcards for complex data structures.
Each element can be a string or InterpolatedString for dynamic values.
"""
config: Config
"""
Configuration object containing connection and extraction parameters.
Used for interpolating dynamic values in field paths.
"""
parameters: InitVar[Mapping[str, Any]]
"""
Initialization parameters passed during object creation.
Used to configure InterpolatedString objects in field_path.
"""
decoder: Decoder = JsonDecoder(parameters={})
"""
Response decoder for converting HTTP response to Python objects.
Defaults to JsonDecoder for JSON API responses.
"""
def __post_init__(self, parameters: Mapping[str, Any]):
"""
Initialize InterpolatedString objects in field_path after creation.
Args:
parameters: Parameters for configuring dynamic string interpolation
"""
def extract_records(self, response: requests.Response) -> List[Mapping[str, Any]]:
"""
Extract and process records from HTTP response.
Decodes the response, extracts records using the configured field path,
applies date format conversion, and returns processed records.
Args:
response: HTTP response object containing JSON data
Returns:
List[Mapping[str, Any]]: List of extracted records with converted dates
Empty list if no records found or extraction fails
Processing Steps:
1. Decode HTTP response using configured decoder
2. Extract records using field_path (supports nested paths and wildcards)
3. Apply date format conversion using ParseDates.convert_dates()
4. Return list of processed records
Examples:
# Response: {"BankTransactions": [{"ID": "123", "Date": "/Date(1419937200000)/"}]}
# field_path: ["BankTransactions"]
# Returns: [{"ID": "123", "Date": "2014-12-30T07:00:00+00:00"}]
"""from source_xero.components import ParseDates
from datetime import datetime, timezone
# Parse individual date strings
xero_date = "/Date(1419937200000+0000)/"
parsed = ParseDates.parse_date(xero_date)
print(parsed) # 2014-12-30 07:00:00+00:00
# Handle timezone offsets
date_with_offset = "/Date(1580628711500+0300)/"
parsed_offset = ParseDates.parse_date(date_with_offset)
print(parsed_offset) # 2020-02-02 10:31:51.500000+00:00
# Handle invalid dates gracefully
invalid_date = "not a date"
result = ParseDates.parse_date(invalid_date)
print(result) # Nonefrom source_xero.components import ParseDates
# Convert dates in nested data structures
bank_transaction = {
"BankTransactionID": "12345",
"Date": "/Date(1419937200000+0000)/",
"UpdatedDateUTC": "/Date(1580628711500+0300)/",
"Amount": 150.75,
"LineItems": [
{
"LineItemID": "67890",
"UpdatedDate": "/Date(1419937200000+0000)/",
"Amount": 75.50
}
]
}
# Convert all dates in-place
ParseDates.convert_dates(bank_transaction)
print(bank_transaction)
# Output:
# {
# "BankTransactionID": "12345",
# "Date": "2014-12-30T07:00:00+00:00",
# "UpdatedDateUTC": "2020-02-02T10:31:51+00:00",
# "Amount": 150.75,
# "LineItems": [
# {
# "LineItemID": "67890",
# "UpdatedDate": "2014-12-30T07:00:00+00:00",
# "Amount": 75.50
# }
# ]
# }from source_xero.components import CustomExtractor
from airbyte_cdk.sources.declarative.interpolation import InterpolatedString
import requests
# Create custom extractor for bank transactions
extractor = CustomExtractor(
field_path=["BankTransactions"],
config={"tenant_id": "your-tenant-id"},
parameters={}
)
# Mock response (would come from actual API call)
response = requests.Response()
response._content = b'{"BankTransactions": [{"ID": "123", "Date": "/Date(1419937200000)/"}]}'
response.status_code = 200
# Extract records with automatic date conversion
records = extractor.extract_records(response)
print(records)
# Output: [{"ID": "123", "Date": "2014-12-30T07:00:00+00:00"}]The CustomExtractor is used within the declarative manifest configuration:
# From manifest.yaml
selector:
type: RecordSelector
extractor:
type: CustomRecordExtractor
class_name: source_xero.components.CustomExtractor
field_path: ["{{ parameters.extractor_path }}"]This allows streams to specify their extraction path dynamically:
bank_transactions_stream:
$parameters:
extractor_path: "BankTransactions" # Extracts from response.BankTransactionsThe data processing components include robust error handling:
Install with Tessl CLI
npx tessl i tessl/pypi-airbyte-source-xero