airbyte-agent-jira 0.1.22__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- airbyte_agent_jira/__init__.py +91 -0
- airbyte_agent_jira/_vendored/__init__.py +1 -0
- airbyte_agent_jira/_vendored/connector_sdk/__init__.py +82 -0
- airbyte_agent_jira/_vendored/connector_sdk/auth_strategies.py +1123 -0
- airbyte_agent_jira/_vendored/connector_sdk/auth_template.py +135 -0
- airbyte_agent_jira/_vendored/connector_sdk/cloud_utils/__init__.py +5 -0
- airbyte_agent_jira/_vendored/connector_sdk/cloud_utils/client.py +213 -0
- airbyte_agent_jira/_vendored/connector_sdk/connector_model_loader.py +965 -0
- airbyte_agent_jira/_vendored/connector_sdk/constants.py +78 -0
- airbyte_agent_jira/_vendored/connector_sdk/exceptions.py +23 -0
- airbyte_agent_jira/_vendored/connector_sdk/executor/__init__.py +31 -0
- airbyte_agent_jira/_vendored/connector_sdk/executor/hosted_executor.py +197 -0
- airbyte_agent_jira/_vendored/connector_sdk/executor/local_executor.py +1574 -0
- airbyte_agent_jira/_vendored/connector_sdk/executor/models.py +190 -0
- airbyte_agent_jira/_vendored/connector_sdk/extensions.py +694 -0
- airbyte_agent_jira/_vendored/connector_sdk/http/__init__.py +37 -0
- airbyte_agent_jira/_vendored/connector_sdk/http/adapters/__init__.py +9 -0
- airbyte_agent_jira/_vendored/connector_sdk/http/adapters/httpx_adapter.py +251 -0
- airbyte_agent_jira/_vendored/connector_sdk/http/config.py +98 -0
- airbyte_agent_jira/_vendored/connector_sdk/http/exceptions.py +119 -0
- airbyte_agent_jira/_vendored/connector_sdk/http/protocols.py +114 -0
- airbyte_agent_jira/_vendored/connector_sdk/http/response.py +102 -0
- airbyte_agent_jira/_vendored/connector_sdk/http_client.py +686 -0
- airbyte_agent_jira/_vendored/connector_sdk/introspection.py +262 -0
- airbyte_agent_jira/_vendored/connector_sdk/logging/__init__.py +11 -0
- airbyte_agent_jira/_vendored/connector_sdk/logging/logger.py +264 -0
- airbyte_agent_jira/_vendored/connector_sdk/logging/types.py +92 -0
- airbyte_agent_jira/_vendored/connector_sdk/observability/__init__.py +11 -0
- airbyte_agent_jira/_vendored/connector_sdk/observability/models.py +19 -0
- airbyte_agent_jira/_vendored/connector_sdk/observability/redactor.py +81 -0
- airbyte_agent_jira/_vendored/connector_sdk/observability/session.py +94 -0
- airbyte_agent_jira/_vendored/connector_sdk/performance/__init__.py +6 -0
- airbyte_agent_jira/_vendored/connector_sdk/performance/instrumentation.py +57 -0
- airbyte_agent_jira/_vendored/connector_sdk/performance/metrics.py +93 -0
- airbyte_agent_jira/_vendored/connector_sdk/schema/__init__.py +75 -0
- airbyte_agent_jira/_vendored/connector_sdk/schema/base.py +161 -0
- airbyte_agent_jira/_vendored/connector_sdk/schema/components.py +239 -0
- airbyte_agent_jira/_vendored/connector_sdk/schema/connector.py +131 -0
- airbyte_agent_jira/_vendored/connector_sdk/schema/extensions.py +109 -0
- airbyte_agent_jira/_vendored/connector_sdk/schema/operations.py +146 -0
- airbyte_agent_jira/_vendored/connector_sdk/schema/security.py +223 -0
- airbyte_agent_jira/_vendored/connector_sdk/secrets.py +182 -0
- airbyte_agent_jira/_vendored/connector_sdk/telemetry/__init__.py +10 -0
- airbyte_agent_jira/_vendored/connector_sdk/telemetry/config.py +32 -0
- airbyte_agent_jira/_vendored/connector_sdk/telemetry/events.py +58 -0
- airbyte_agent_jira/_vendored/connector_sdk/telemetry/tracker.py +151 -0
- airbyte_agent_jira/_vendored/connector_sdk/types.py +245 -0
- airbyte_agent_jira/_vendored/connector_sdk/utils.py +60 -0
- airbyte_agent_jira/_vendored/connector_sdk/validation.py +822 -0
- airbyte_agent_jira/connector.py +978 -0
- airbyte_agent_jira/connector_model.py +2827 -0
- airbyte_agent_jira/models.py +741 -0
- airbyte_agent_jira/types.py +117 -0
- airbyte_agent_jira-0.1.22.dist-info/METADATA +113 -0
- airbyte_agent_jira-0.1.22.dist-info/RECORD +56 -0
- airbyte_agent_jira-0.1.22.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Shared redaction logic for both logging and telemetry."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict
|
|
4
|
+
from urllib.parse import parse_qs, urlencode, urlparse, urlunparse
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class DataRedactor:
|
|
8
|
+
"""Shared redaction logic for both logging and telemetry."""
|
|
9
|
+
|
|
10
|
+
SENSITIVE_HEADER_PATTERNS = [
|
|
11
|
+
"authorization",
|
|
12
|
+
"api-key",
|
|
13
|
+
"x-api-key",
|
|
14
|
+
"token",
|
|
15
|
+
"bearer",
|
|
16
|
+
"secret",
|
|
17
|
+
"password",
|
|
18
|
+
"credential",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
SENSITIVE_PARAM_PATTERNS = [
|
|
22
|
+
"password",
|
|
23
|
+
"secret",
|
|
24
|
+
"api_key",
|
|
25
|
+
"apikey",
|
|
26
|
+
"token",
|
|
27
|
+
"credentials",
|
|
28
|
+
"auth",
|
|
29
|
+
"key",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
@staticmethod
|
|
33
|
+
def redact_headers(headers: Dict[str, str]) -> Dict[str, str]:
|
|
34
|
+
"""Redact sensitive headers."""
|
|
35
|
+
redacted = {}
|
|
36
|
+
for key, value in headers.items():
|
|
37
|
+
if any(pattern in key.lower() for pattern in DataRedactor.SENSITIVE_HEADER_PATTERNS):
|
|
38
|
+
redacted[key] = "***REDACTED***"
|
|
39
|
+
else:
|
|
40
|
+
redacted[key] = value
|
|
41
|
+
return redacted
|
|
42
|
+
|
|
43
|
+
@staticmethod
|
|
44
|
+
def redact_params(params: Dict[str, Any]) -> Dict[str, Any]:
|
|
45
|
+
"""Redact sensitive parameters."""
|
|
46
|
+
redacted = {}
|
|
47
|
+
for key, value in params.items():
|
|
48
|
+
if any(pattern in key.lower() for pattern in DataRedactor.SENSITIVE_PARAM_PATTERNS):
|
|
49
|
+
redacted[key] = "***REDACTED***"
|
|
50
|
+
else:
|
|
51
|
+
redacted[key] = value
|
|
52
|
+
return redacted
|
|
53
|
+
|
|
54
|
+
@staticmethod
|
|
55
|
+
def redact_url(url: str) -> str:
|
|
56
|
+
"""Redact sensitive query params from URL."""
|
|
57
|
+
parsed = urlparse(url)
|
|
58
|
+
if not parsed.query:
|
|
59
|
+
return url
|
|
60
|
+
|
|
61
|
+
params = parse_qs(parsed.query)
|
|
62
|
+
redacted_params = {}
|
|
63
|
+
|
|
64
|
+
for key, values in params.items():
|
|
65
|
+
if any(pattern in key.lower() for pattern in DataRedactor.SENSITIVE_PARAM_PATTERNS):
|
|
66
|
+
redacted_params[key] = ["***REDACTED***"] * len(values)
|
|
67
|
+
else:
|
|
68
|
+
redacted_params[key] = values
|
|
69
|
+
|
|
70
|
+
# Reconstruct URL with redacted params
|
|
71
|
+
new_query = urlencode(redacted_params, doseq=True)
|
|
72
|
+
return urlunparse(
|
|
73
|
+
(
|
|
74
|
+
parsed.scheme,
|
|
75
|
+
parsed.netloc,
|
|
76
|
+
parsed.path,
|
|
77
|
+
parsed.params,
|
|
78
|
+
new_query,
|
|
79
|
+
parsed.fragment,
|
|
80
|
+
)
|
|
81
|
+
)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Shared session context for both logging and telemetry."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import uuid
|
|
5
|
+
from datetime import UTC, datetime
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Dict, Optional
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_persistent_user_id() -> str:
|
|
13
|
+
"""
|
|
14
|
+
Get or create an anonymous user ID stored in the home directory.
|
|
15
|
+
|
|
16
|
+
The ID is stored in ~/.airbyte/ai_sdk_user_id and persists across all sessions.
|
|
17
|
+
If the file doesn't exist, a new UUID is generated and saved.
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
An anonymous UUID string that uniquely identifies this user across sessions.
|
|
21
|
+
"""
|
|
22
|
+
try:
|
|
23
|
+
# Create .airbyte directory in home folder if it doesn't exist
|
|
24
|
+
airbyte_dir = Path.home() / ".airbyte"
|
|
25
|
+
airbyte_dir.mkdir(exist_ok=True)
|
|
26
|
+
|
|
27
|
+
# Path to user ID file
|
|
28
|
+
user_id_file = airbyte_dir / "ai_sdk_user_id"
|
|
29
|
+
|
|
30
|
+
# Try to read existing user ID
|
|
31
|
+
if user_id_file.exists():
|
|
32
|
+
user_id = user_id_file.read_text().strip()
|
|
33
|
+
if user_id: # Validate it's not empty
|
|
34
|
+
return user_id
|
|
35
|
+
|
|
36
|
+
# Generate new user ID if file doesn't exist or is empty
|
|
37
|
+
user_id = str(uuid.uuid4())
|
|
38
|
+
user_id_file.write_text(user_id)
|
|
39
|
+
logger.debug(f"Generated new anonymous user ID: {user_id}")
|
|
40
|
+
|
|
41
|
+
return user_id
|
|
42
|
+
except Exception as e:
|
|
43
|
+
# If we can't read/write the file, generate a session-only ID
|
|
44
|
+
logger.debug(f"Could not access anonymous user ID file: {e}")
|
|
45
|
+
return str(uuid.uuid4())
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def get_public_ip() -> Optional[str]:
|
|
49
|
+
"""
|
|
50
|
+
Fetch the public IP address of the user.
|
|
51
|
+
|
|
52
|
+
Returns None if unable to fetch (network issues, etc).
|
|
53
|
+
Uses httpx for a robust HTTP request to a public IP service.
|
|
54
|
+
"""
|
|
55
|
+
try:
|
|
56
|
+
import httpx
|
|
57
|
+
|
|
58
|
+
# Use a short timeout to avoid blocking
|
|
59
|
+
with httpx.Client(timeout=2.0) as client:
|
|
60
|
+
response = client.get("https://api.ipify.org?format=text")
|
|
61
|
+
response.raise_for_status()
|
|
62
|
+
return response.text.strip()
|
|
63
|
+
except Exception:
|
|
64
|
+
# Never fail - just return None
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class ObservabilitySession:
|
|
69
|
+
"""Shared session context for both logging and telemetry."""
|
|
70
|
+
|
|
71
|
+
def __init__(
|
|
72
|
+
self,
|
|
73
|
+
connector_name: str,
|
|
74
|
+
connector_version: Optional[str] = None,
|
|
75
|
+
execution_context: str = "direct",
|
|
76
|
+
session_id: Optional[str] = None,
|
|
77
|
+
):
|
|
78
|
+
self.session_id = session_id or str(uuid.uuid4())
|
|
79
|
+
self.user_id = get_persistent_user_id()
|
|
80
|
+
self.connector_name = connector_name
|
|
81
|
+
self.connector_version = connector_version
|
|
82
|
+
self.execution_context = execution_context
|
|
83
|
+
self.started_at = datetime.now(UTC)
|
|
84
|
+
self.operation_count = 0
|
|
85
|
+
self.metadata: Dict[str, Any] = {}
|
|
86
|
+
self.public_ip = get_public_ip()
|
|
87
|
+
|
|
88
|
+
def increment_operations(self):
|
|
89
|
+
"""Increment the operation counter."""
|
|
90
|
+
self.operation_count += 1
|
|
91
|
+
|
|
92
|
+
def duration_seconds(self) -> float:
|
|
93
|
+
"""Calculate session duration in seconds."""
|
|
94
|
+
return (datetime.now(UTC) - self.started_at).total_seconds()
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Performance instrumentation decorator for async functions."""
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
import logging
|
|
5
|
+
import time
|
|
6
|
+
from typing import Any, Callable, TypeVar
|
|
7
|
+
|
|
8
|
+
# Type variable for generic function decoration
|
|
9
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def instrument(metric_name: str) -> Callable[[F], F]:
|
|
15
|
+
"""Decorator to instrument async functions with performance tracking.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
metric_name: Name of the metric to track
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
Decorator function
|
|
22
|
+
|
|
23
|
+
Example:
|
|
24
|
+
@instrument("stripe.customer.list")
|
|
25
|
+
async def list_customers():
|
|
26
|
+
...
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def decorator(func: F) -> F:
|
|
30
|
+
@functools.wraps(func)
|
|
31
|
+
async def wrapper(*args, **kwargs):
|
|
32
|
+
start_time = time.time()
|
|
33
|
+
success = True
|
|
34
|
+
error = None
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
result = await func(*args, **kwargs)
|
|
38
|
+
return result
|
|
39
|
+
|
|
40
|
+
except Exception as e:
|
|
41
|
+
success = False
|
|
42
|
+
error = e
|
|
43
|
+
raise
|
|
44
|
+
|
|
45
|
+
finally:
|
|
46
|
+
duration = time.time() - start_time
|
|
47
|
+
duration_ms = duration * 1000
|
|
48
|
+
|
|
49
|
+
# Log performance metrics
|
|
50
|
+
if success:
|
|
51
|
+
logger.debug(f"[{metric_name}] completed in {duration_ms:.2f}ms")
|
|
52
|
+
else:
|
|
53
|
+
logger.warning(f"[{metric_name}] failed after {duration_ms:.2f}ms: {error}")
|
|
54
|
+
|
|
55
|
+
return wrapper # type: ignore
|
|
56
|
+
|
|
57
|
+
return decorator
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Performance metrics tracking."""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from contextlib import asynccontextmanager
|
|
5
|
+
from typing import Dict, Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class PerformanceMonitor:
|
|
9
|
+
"""Monitor and track performance metrics for operations."""
|
|
10
|
+
|
|
11
|
+
def __init__(self):
|
|
12
|
+
"""Initialize performance monitor."""
|
|
13
|
+
self._metrics: Dict[str, Dict[str, float]] = {}
|
|
14
|
+
|
|
15
|
+
def record(self, metric_name: str, duration: float):
|
|
16
|
+
"""Record a metric.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
metric_name: Name of the metric
|
|
20
|
+
duration: Duration in seconds
|
|
21
|
+
"""
|
|
22
|
+
if metric_name not in self._metrics:
|
|
23
|
+
self._metrics[metric_name] = {
|
|
24
|
+
"count": 0,
|
|
25
|
+
"total": 0.0,
|
|
26
|
+
"min": float("inf"),
|
|
27
|
+
"max": 0.0,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
metrics = self._metrics[metric_name]
|
|
31
|
+
metrics["count"] += 1
|
|
32
|
+
metrics["total"] += duration
|
|
33
|
+
metrics["min"] = min(metrics["min"], duration)
|
|
34
|
+
metrics["max"] = max(metrics["max"], duration)
|
|
35
|
+
|
|
36
|
+
def get_stats(self, metric_name: str) -> Optional[Dict[str, float]]:
|
|
37
|
+
"""Get statistics for a metric.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
metric_name: Name of the metric
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Dictionary with count, total, mean, min, max or None if metric not found
|
|
44
|
+
"""
|
|
45
|
+
if metric_name not in self._metrics:
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
metrics = self._metrics[metric_name]
|
|
49
|
+
return {
|
|
50
|
+
"count": metrics["count"],
|
|
51
|
+
"total": metrics["total"],
|
|
52
|
+
"mean": metrics["total"] / metrics["count"] if metrics["count"] > 0 else 0.0,
|
|
53
|
+
"min": metrics["min"] if metrics["min"] != float("inf") else 0.0,
|
|
54
|
+
"max": metrics["max"],
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
def get_all_stats(self) -> Dict[str, Dict[str, float]]:
|
|
58
|
+
"""Get statistics for all metrics.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Dictionary mapping metric names to their statistics
|
|
62
|
+
"""
|
|
63
|
+
return {name: self.get_stats(name) for name in self._metrics.keys()}
|
|
64
|
+
|
|
65
|
+
def reset(self, metric_name: Optional[str] = None):
|
|
66
|
+
"""Reset metrics.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
metric_name: Specific metric to reset, or None to reset all
|
|
70
|
+
"""
|
|
71
|
+
if metric_name:
|
|
72
|
+
if metric_name in self._metrics:
|
|
73
|
+
del self._metrics[metric_name]
|
|
74
|
+
else:
|
|
75
|
+
self._metrics.clear()
|
|
76
|
+
|
|
77
|
+
@asynccontextmanager
|
|
78
|
+
async def track(self, metric_name: str):
|
|
79
|
+
"""Context manager for tracking operation duration.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
metric_name: Name of the metric to track
|
|
83
|
+
|
|
84
|
+
Example:
|
|
85
|
+
async with monitor.track("api_call"):
|
|
86
|
+
result = await some_async_operation()
|
|
87
|
+
"""
|
|
88
|
+
start_time = time.time()
|
|
89
|
+
try:
|
|
90
|
+
yield
|
|
91
|
+
finally:
|
|
92
|
+
duration = time.time() - start_time
|
|
93
|
+
self.record(metric_name, duration)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pydantic 2 schema models for OpenAPI 3.0 connector specifications.
|
|
3
|
+
|
|
4
|
+
This package provides strongly-typed Pydantic models that mirror the OpenAPI 3.0
|
|
5
|
+
specification while supporting Airbyte-specific extensions.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
import yaml
|
|
9
|
+
from . import OpenAPIConnector
|
|
10
|
+
|
|
11
|
+
with open('connector.yaml') as f:
|
|
12
|
+
data = yaml.safe_load(f)
|
|
13
|
+
|
|
14
|
+
connector = OpenAPIConnector(**data)
|
|
15
|
+
print(connector.list_resources())
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from .base import Contact, Info, License, Server, ServerVariable
|
|
19
|
+
from .components import (
|
|
20
|
+
Components,
|
|
21
|
+
Header,
|
|
22
|
+
MediaType,
|
|
23
|
+
Parameter,
|
|
24
|
+
RequestBody,
|
|
25
|
+
Response,
|
|
26
|
+
Schema,
|
|
27
|
+
)
|
|
28
|
+
from .connector import ExternalDocs, OpenAPIConnector, Tag
|
|
29
|
+
from .extensions import PaginationConfig, RateLimitConfig, RetryConfig
|
|
30
|
+
from .operations import Operation, PathItem
|
|
31
|
+
from .security import (
|
|
32
|
+
AirbyteAuthConfig,
|
|
33
|
+
AuthConfigFieldSpec,
|
|
34
|
+
AuthConfigOption,
|
|
35
|
+
OAuth2Flow,
|
|
36
|
+
OAuth2Flows,
|
|
37
|
+
SecurityRequirement,
|
|
38
|
+
SecurityScheme,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
__all__ = [
|
|
42
|
+
# Root model
|
|
43
|
+
"OpenAPIConnector",
|
|
44
|
+
"Tag",
|
|
45
|
+
"ExternalDocs",
|
|
46
|
+
# Base models
|
|
47
|
+
"Info",
|
|
48
|
+
"Server",
|
|
49
|
+
"ServerVariable",
|
|
50
|
+
"Contact",
|
|
51
|
+
"License",
|
|
52
|
+
# Security models
|
|
53
|
+
"SecurityScheme",
|
|
54
|
+
"SecurityRequirement",
|
|
55
|
+
"OAuth2Flow",
|
|
56
|
+
"OAuth2Flows",
|
|
57
|
+
"AirbyteAuthConfig",
|
|
58
|
+
"AuthConfigOption",
|
|
59
|
+
"AuthConfigFieldSpec",
|
|
60
|
+
# Component models
|
|
61
|
+
"Components",
|
|
62
|
+
"Schema",
|
|
63
|
+
"Parameter",
|
|
64
|
+
"RequestBody",
|
|
65
|
+
"Response",
|
|
66
|
+
"MediaType",
|
|
67
|
+
"Header",
|
|
68
|
+
# Operation models
|
|
69
|
+
"PathItem",
|
|
70
|
+
"Operation",
|
|
71
|
+
# Extension models (for future use)
|
|
72
|
+
"PaginationConfig",
|
|
73
|
+
"RateLimitConfig",
|
|
74
|
+
"RetryConfig",
|
|
75
|
+
]
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base OpenAPI 3.1 models: Info, Server, Contact, License.
|
|
3
|
+
|
|
4
|
+
References:
|
|
5
|
+
- https://spec.openapis.org/oas/v3.1.0#info-object
|
|
6
|
+
- https://spec.openapis.org/oas/v3.1.0#server-object
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from enum import StrEnum
|
|
10
|
+
from typing import Dict, Optional
|
|
11
|
+
from uuid import UUID
|
|
12
|
+
|
|
13
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
14
|
+
from pydantic_core import Url
|
|
15
|
+
|
|
16
|
+
from .extensions import RetryConfig
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ExampleQuestions(BaseModel):
|
|
20
|
+
"""
|
|
21
|
+
Example questions for AI connector documentation.
|
|
22
|
+
|
|
23
|
+
Used to generate supported_questions.md and unsupported_questions.md files
|
|
24
|
+
that appear in the connector's README.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
|
28
|
+
|
|
29
|
+
supported: list[str] = Field(
|
|
30
|
+
default_factory=list,
|
|
31
|
+
description="Example questions the connector can handle",
|
|
32
|
+
)
|
|
33
|
+
unsupported: list[str] = Field(
|
|
34
|
+
default_factory=list,
|
|
35
|
+
description="Example questions the connector cannot handle",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Contact(BaseModel):
|
|
40
|
+
"""
|
|
41
|
+
Contact information for the API.
|
|
42
|
+
|
|
43
|
+
OpenAPI Reference: https://spec.openapis.org/oas/v3.1.0#contact-object
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
|
47
|
+
|
|
48
|
+
name: Optional[str] = None
|
|
49
|
+
url: Optional[str] = None
|
|
50
|
+
email: Optional[str] = None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class License(BaseModel):
|
|
54
|
+
"""
|
|
55
|
+
License information for the API.
|
|
56
|
+
|
|
57
|
+
OpenAPI Reference: https://spec.openapis.org/oas/v3.1.0#license-object
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
|
61
|
+
|
|
62
|
+
name: str
|
|
63
|
+
url: Optional[str] = None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class DocUrlType(StrEnum):
|
|
67
|
+
API_DEPRECATIONS = "api_deprecations"
|
|
68
|
+
API_REFERENCE = "api_reference"
|
|
69
|
+
API_RELEASE_HISTORY = "api_release_history"
|
|
70
|
+
AUTHENTICATION_GUIDE = "authentication_guide"
|
|
71
|
+
CHANGELOG = "changelog"
|
|
72
|
+
DATA_MODEL_REFERENCE = "data_model_reference"
|
|
73
|
+
DEVELOPER_COMMUNITY = "developer_community"
|
|
74
|
+
MIGRATION_GUIDE = "migration_guide"
|
|
75
|
+
OPENAPI_SPEC = "openapi_spec"
|
|
76
|
+
OTHER = "other"
|
|
77
|
+
PERMISSIONS_SCOPES = "permissions_scopes"
|
|
78
|
+
RATE_LIMITS = "rate_limits"
|
|
79
|
+
SQL_REFERENCE = "sql_reference"
|
|
80
|
+
STATUS_PAGE = "status_page"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class DocUrl(BaseModel):
|
|
84
|
+
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
|
85
|
+
|
|
86
|
+
url: str
|
|
87
|
+
type: DocUrlType
|
|
88
|
+
title: Optional[str] = None
|
|
89
|
+
|
|
90
|
+
@field_validator("url")
|
|
91
|
+
def validate_url(cls, v):
|
|
92
|
+
Url(v)
|
|
93
|
+
return v
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class Info(BaseModel):
|
|
97
|
+
"""
|
|
98
|
+
API metadata information.
|
|
99
|
+
|
|
100
|
+
OpenAPI Reference: https://spec.openapis.org/oas/v3.1.0#info-object
|
|
101
|
+
|
|
102
|
+
Extensions:
|
|
103
|
+
- x-airbyte-connector-name: Name of the connector (Airbyte extension)
|
|
104
|
+
- x-airbyte-connector-id: UUID of the connector (Airbyte extension)
|
|
105
|
+
- x-airbyte-external-documentation-urls: List of external documentation URLs (Airbyte extension)
|
|
106
|
+
- x-airbyte-retry-config: Retry configuration for transient errors (Airbyte extension)
|
|
107
|
+
- x-airbyte-example-questions: Example questions for AI connector README (Airbyte extension)
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
|
111
|
+
|
|
112
|
+
title: str
|
|
113
|
+
version: str
|
|
114
|
+
description: Optional[str] = None
|
|
115
|
+
terms_of_service: Optional[str] = Field(None, alias="termsOfService")
|
|
116
|
+
contact: Optional[Contact] = None
|
|
117
|
+
license: Optional[License] = None
|
|
118
|
+
|
|
119
|
+
# Airbyte extension
|
|
120
|
+
x_airbyte_connector_name: Optional[str] = Field(None, alias="x-airbyte-connector-name")
|
|
121
|
+
x_airbyte_connector_id: Optional[UUID] = Field(None, alias="x-airbyte-connector-id")
|
|
122
|
+
x_airbyte_external_documentation_urls: list[DocUrl] = Field(..., alias="x-airbyte-external-documentation-urls")
|
|
123
|
+
x_airbyte_retry_config: Optional[RetryConfig] = Field(None, alias="x-airbyte-retry-config")
|
|
124
|
+
x_airbyte_example_questions: Optional[ExampleQuestions] = Field(None, alias="x-airbyte-example-questions")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class ServerVariable(BaseModel):
|
|
128
|
+
"""
|
|
129
|
+
Variable for server URL templating.
|
|
130
|
+
|
|
131
|
+
OpenAPI Reference: https://spec.openapis.org/oas/v3.1.0#server-variable-object
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
|
135
|
+
|
|
136
|
+
enum: Optional[list[str]] = None
|
|
137
|
+
default: str
|
|
138
|
+
description: Optional[str] = None
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class Server(BaseModel):
|
|
142
|
+
"""
|
|
143
|
+
Server URL and variable definitions.
|
|
144
|
+
|
|
145
|
+
OpenAPI Reference: https://spec.openapis.org/oas/v3.1.0#server-object
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
|
149
|
+
|
|
150
|
+
url: str
|
|
151
|
+
description: Optional[str] = None
|
|
152
|
+
variables: Dict[str, ServerVariable] = Field(default_factory=dict)
|
|
153
|
+
|
|
154
|
+
@field_validator("url")
|
|
155
|
+
@classmethod
|
|
156
|
+
def validate_url(cls, v: str) -> str:
|
|
157
|
+
"""Validate that server URL is properly formatted."""
|
|
158
|
+
if not v:
|
|
159
|
+
raise ValueError("Server URL cannot be empty")
|
|
160
|
+
# Allow both absolute URLs and relative paths
|
|
161
|
+
return v
|