Files
TimeTracker/app/integrations/base.py
T
Dries Peeters 53c58ec43e feat: comprehensive integration setup and configuration system
Enhanced all integrations with complete setup procedures, authentication flows,
and comprehensive configuration management.

Base Connector Enhancements:
- Extended get_config_schema() with sections, sync settings, and validation
- Added validate_config() with type checking and constraints
- Added helper methods: get_sync_settings(), get_field_mappings(), get_status_mappings()

Integration Configuration Schemas:
- All integrations now have complete config schemas with organized sections
- Support for sync direction (Import/Export/Bidirectional)
- Sync scheduling options (Manual/Hourly/Daily/Weekly)
- Data mapping configuration (status mappings, field mappings)
- Field types: string, number, boolean, select, array, json, url, text, password
- Comprehensive help text and descriptions for all fields

Enhanced Integrations:
- Jira: JQL queries, status mapping, project auto-creation
- GitHub: Repository selection, issue state filtering, webhook security
- GitLab: Project selection, issue filtering, webhook configuration
- Slack: Channel selection, notification triggers
- Asana: Workspace/project selection, completion status sync
- Trello: Board selection, list-to-status mapping
- Microsoft Teams: Channel configuration, notification settings
- QuickBooks: Customer/item/account mappings, sandbox mode
- Xero: Contact/item/account mappings
- Google Calendar: Event formatting, date range controls
- Outlook Calendar: Event formatting, date range controls
- CalDAV: Server discovery, SSL verification, lookback/lookahead

UI Enhancements:
- Section-based configuration display
- Support for all field types (select, array, number, json, boolean)
- Improved help text and descriptions
- Better visual organization and validation

Route Enhancements:
- Config schema passed to template
- Form processing for all field types
- Proper default value handling
- Validation error messages

This provides a complete, user-friendly integration setup experience with
one-button OAuth connections, configurable sync settings, and data translation
capabilities.
2025-12-29 16:35:06 +01:00

300 lines
11 KiB
Python

"""
Base connector interface for integrations.
"""
from abc import ABC, abstractmethod
from typing import Dict, Any, Optional, List
from datetime import datetime
class BaseConnector(ABC):
"""
Base class for all integration connectors.
All connectors must implement these methods to provide
a consistent interface for integration management.
"""
def __init__(self, integration, credentials):
"""
Initialize connector with integration and credentials.
Args:
integration: Integration model instance
credentials: IntegrationCredential model instance
"""
self.integration = integration
self.credentials = credentials
@property
@abstractmethod
def provider_name(self) -> str:
"""Return the provider name (e.g., 'jira', 'slack', 'github')."""
pass
@property
@abstractmethod
def display_name(self) -> str:
"""Return the display name for the provider."""
pass
@abstractmethod
def get_authorization_url(self, redirect_uri: str, state: str = None) -> str:
"""
Get OAuth authorization URL.
Args:
redirect_uri: OAuth callback URL
state: Optional state parameter for CSRF protection
Returns:
Authorization URL
"""
pass
@abstractmethod
def exchange_code_for_tokens(self, code: str, redirect_uri: str) -> Dict[str, Any]:
"""
Exchange authorization code for access tokens.
Args:
code: Authorization code from OAuth callback
redirect_uri: OAuth callback URL
Returns:
Dict with access_token, refresh_token, expires_at, etc.
"""
pass
@abstractmethod
def refresh_access_token(self) -> Dict[str, Any]:
"""
Refresh access token using refresh token.
Returns:
Dict with new access_token, expires_at, etc.
"""
pass
@abstractmethod
def test_connection(self) -> Dict[str, Any]:
"""
Test the connection to the service.
Returns:
Dict with 'success' (bool) and 'message' (str)
"""
pass
def get_access_token(self) -> Optional[str]:
"""
Get current access token, refreshing if needed.
Returns:
Access token string or None
"""
if not self.credentials:
return None
# Check if token needs refresh
if self.credentials.needs_refresh():
try:
new_tokens = self.refresh_access_token()
if new_tokens.get("access_token"):
return new_tokens["access_token"]
except Exception:
pass
return self.credentials.access_token if self.credentials else None
def sync_data(self, sync_type: str = "full") -> Dict[str, Any]:
"""
Sync data from the integrated service.
Args:
sync_type: Type of sync ('full', 'incremental', etc.)
Returns:
Dict with sync results
"""
# Default implementation - override in subclasses
return {"success": False, "message": "Sync not implemented for this connector"}
def handle_webhook(self, payload: Dict[str, Any], headers: Dict[str, str], raw_body: Optional[bytes] = None) -> Dict[str, Any]:
"""
Handle incoming webhook from the service.
Args:
payload: Webhook payload (parsed JSON/dict)
headers: Request headers
raw_body: Raw request body bytes (for signature verification)
Returns:
Dict with processing results
"""
# Default implementation - override in subclasses
return {"success": False, "message": "Webhook handling not implemented for this connector"}
def get_config_schema(self) -> Dict[str, Any]:
"""
Get configuration schema for this connector.
Returns:
Dict describing configuration fields with structure:
{
"fields": [
{
"name": "field_name",
"type": "string|number|boolean|select|array|json|password|url|text",
"label": "Display Label",
"description": "Help text",
"placeholder": "Placeholder text",
"default": default_value,
"required": True/False,
"options": [{"value": "val", "label": "Label"}] for select,
"help": "Additional help text",
"validation": {"min": 1, "max": 100} for numbers,
}
],
"required": ["field_name"],
"sections": [
{
"title": "Section Title",
"description": "Section description",
"fields": ["field_name1", "field_name2"]
}
],
"sync_settings": {
"enabled": True/False,
"auto_sync": True/False,
"sync_interval": "hourly|daily|weekly|manual",
"sync_direction": "provider_to_timetracker|timetracker_to_provider|bidirectional",
"sync_items": ["tasks", "projects", "time_entries"],
}
}
"""
return {
"fields": [],
"required": [],
"sections": [],
"sync_settings": {
"enabled": True,
"auto_sync": False,
"sync_interval": "manual",
"sync_direction": "provider_to_timetracker",
"sync_items": [],
}
}
def validate_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
"""
Validate configuration.
Args:
config: Configuration dict to validate
Returns:
Dict with 'valid' (bool) and 'errors' (list)
"""
schema = self.get_config_schema()
errors = []
# Check required fields
required_fields = schema.get("required", [])
for field_name in required_fields:
if field_name not in config or not config[field_name]:
# Find field label for error message
field_label = field_name
for field in schema.get("fields", []):
if field.get("name") == field_name:
field_label = field.get("label", field_name)
break
errors.append(f"{field_label} is required")
# Validate field types and constraints
for field in schema.get("fields", []):
field_name = field.get("name")
if field_name not in config:
continue
value = config[field_name]
field_type = field.get("type", "string")
# Type validation
if field_type == "number" and value is not None:
try:
float(value)
# Check min/max if specified
validation = field.get("validation", {})
if "min" in validation and float(value) < validation["min"]:
errors.append(f"{field.get('label', field_name)} must be at least {validation['min']}")
if "max" in validation and float(value) > validation["max"]:
errors.append(f"{field.get('label', field_name)} must be at most {validation['max']}")
except (ValueError, TypeError):
errors.append(f"{field.get('label', field_name)} must be a number")
elif field_type == "boolean" and value is not None:
if not isinstance(value, bool) and value not in ("true", "false", "1", "0", "on", "off", ""):
errors.append(f"{field.get('label', field_name)} must be a boolean")
elif field_type == "url" and value:
try:
from urllib.parse import urlparse
parsed = urlparse(value)
if not parsed.scheme or not parsed.netloc:
errors.append(f"{field.get('label', field_name)} must be a valid URL")
except Exception:
errors.append(f"{field.get('label', field_name)} must be a valid URL")
elif field_type == "json" and value:
try:
import json
if isinstance(value, str):
json.loads(value)
except json.JSONDecodeError:
errors.append(f"{field.get('label', field_name)} must be valid JSON")
return {"valid": len(errors) == 0, "errors": errors}
def get_sync_settings(self) -> Dict[str, Any]:
"""
Get current sync settings from integration config.
Returns:
Dict with sync settings
"""
if not self.integration or not self.integration.config:
schema = self.get_config_schema()
return schema.get("sync_settings", {})
config = self.integration.config
schema = self.get_config_schema()
default_sync_settings = schema.get("sync_settings", {})
return {
"enabled": config.get("sync_enabled", default_sync_settings.get("enabled", True)),
"auto_sync": config.get("auto_sync", default_sync_settings.get("auto_sync", False)),
"sync_interval": config.get("sync_interval", default_sync_settings.get("sync_interval", "manual")),
"sync_direction": config.get("sync_direction", default_sync_settings.get("sync_direction", "provider_to_timetracker")),
"sync_items": config.get("sync_items", default_sync_settings.get("sync_items", [])),
}
def get_field_mappings(self) -> Dict[str, str]:
"""
Get field mappings for data translation.
Returns:
Dict mapping provider fields to TimeTracker fields
"""
if not self.integration or not self.integration.config:
return {}
return self.integration.config.get("field_mappings", {})
def get_status_mappings(self) -> Dict[str, str]:
"""
Get status mappings for data translation.
Returns:
Dict mapping provider statuses to TimeTracker statuses
"""
if not self.integration or not self.integration.config:
return {}
return self.integration.config.get("status_mappings", {})