pyrestkit 1.2.0__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.
Files changed (115) hide show
  1. pyrestkit/__init__.py +35 -0
  2. pyrestkit/ai/__init__.py +17 -0
  3. pyrestkit/ai/analyzer.py +137 -0
  4. pyrestkit/ai/client.py +101 -0
  5. pyrestkit/ai/config/__init__.py +5 -0
  6. pyrestkit/ai/config/ai_config.py +200 -0
  7. pyrestkit/ai/exceptions.py +22 -0
  8. pyrestkit/ai/generators/__init__.py +0 -0
  9. pyrestkit/ai/models.py +44 -0
  10. pyrestkit/ai/parsers/__init__.py +0 -0
  11. pyrestkit/ai/prompts/failure_analysis.md +21 -0
  12. pyrestkit/ai/provider.py +58 -0
  13. pyrestkit/ai/providers/__init__.py +21 -0
  14. pyrestkit/ai/providers/anthropic.py +85 -0
  15. pyrestkit/ai/providers/azure_openai.py +84 -0
  16. pyrestkit/ai/providers/base.py +39 -0
  17. pyrestkit/ai/providers/bedrock.py +70 -0
  18. pyrestkit/ai/providers/cohere.py +82 -0
  19. pyrestkit/ai/providers/gemini.py +113 -0
  20. pyrestkit/ai/providers/groq.py +81 -0
  21. pyrestkit/ai/providers/mistral.py +88 -0
  22. pyrestkit/ai/providers/ollama.py +82 -0
  23. pyrestkit/ai/providers/openai.py +124 -0
  24. pyrestkit/ai/utils/__init__.py +0 -0
  25. pyrestkit/ai/utils/prompt_loader.py +52 -0
  26. pyrestkit/assertions/__init__.py +7 -0
  27. pyrestkit/assertions/assertion_exception.py +4 -0
  28. pyrestkit/assertions/response_assertions.py +181 -0
  29. pyrestkit/auth/__init__.py +11 -0
  30. pyrestkit/auth/auth_strategy.py +14 -0
  31. pyrestkit/auth/authentication_manager.py +35 -0
  32. pyrestkit/auth/strategies/__init__.py +5 -0
  33. pyrestkit/auth/strategies/api_key_auth.py +18 -0
  34. pyrestkit/auth/strategies/basic_auth.py +24 -0
  35. pyrestkit/auth/strategies/bearer_auth.py +17 -0
  36. pyrestkit/auth/token_cache.py +44 -0
  37. pyrestkit/auth/token_manager.py +32 -0
  38. pyrestkit/auth/token_provider.py +12 -0
  39. pyrestkit/auth/token_response.py +13 -0
  40. pyrestkit/builder/__init__.py +5 -0
  41. pyrestkit/builder/fluent_request_builder.py +167 -0
  42. pyrestkit/clients/__init__.py +7 -0
  43. pyrestkit/clients/base_client.py +68 -0
  44. pyrestkit/clients/user_client.py +66 -0
  45. pyrestkit/config/__init__.py +5 -0
  46. pyrestkit/config/config.py +97 -0
  47. pyrestkit/constants/__init__.py +0 -0
  48. pyrestkit/constants/content_types.py +0 -0
  49. pyrestkit/constants/headers.py +0 -0
  50. pyrestkit/constants/status_codes.py +0 -0
  51. pyrestkit/core/__init__.py +13 -0
  52. pyrestkit/core/api_client.py +129 -0
  53. pyrestkit/core/logger.py +41 -0
  54. pyrestkit/core/request_builder.py +45 -0
  55. pyrestkit/core/request_executor.py +64 -0
  56. pyrestkit/core/request_logger.py +0 -0
  57. pyrestkit/core/response_logger.py +0 -0
  58. pyrestkit/core/session_manager.py +19 -0
  59. pyrestkit/database/__init__.py +0 -0
  60. pyrestkit/endpoints/__init__.py +5 -0
  61. pyrestkit/endpoints/base_endpoints.py +32 -0
  62. pyrestkit/endpoints/order_endpoints.py +9 -0
  63. pyrestkit/endpoints/payment_endpoints.py +5 -0
  64. pyrestkit/endpoints/user_endpoints.py +48 -0
  65. pyrestkit/exceptions/__init__.py +21 -0
  66. pyrestkit/exceptions/api_exception.py +8 -0
  67. pyrestkit/exceptions/authentication_exception.py +10 -0
  68. pyrestkit/exceptions/configuration_exception.py +10 -0
  69. pyrestkit/exceptions/exception_mapper.py +32 -0
  70. pyrestkit/exceptions/network_exception.py +10 -0
  71. pyrestkit/exceptions/response_exception.py +10 -0
  72. pyrestkit/exceptions/serialization_exception.py +10 -0
  73. pyrestkit/exceptions/validation_exception.py +10 -0
  74. pyrestkit/factories/__init__.py +5 -0
  75. pyrestkit/factories/base_factory.py +25 -0
  76. pyrestkit/factories/user_factory.py +37 -0
  77. pyrestkit/hooks/__init__.py +5 -0
  78. pyrestkit/hooks/hook.py +27 -0
  79. pyrestkit/hooks/hook_manager.py +39 -0
  80. pyrestkit/hooks/request_hook.py +18 -0
  81. pyrestkit/hooks/response_hook.py +17 -0
  82. pyrestkit/hooks/timing_hook.py +32 -0
  83. pyrestkit/models/__init__.py +8 -0
  84. pyrestkit/models/base_response.py +11 -0
  85. pyrestkit/models/request/__init__.py +7 -0
  86. pyrestkit/models/request/create_user_request.py +11 -0
  87. pyrestkit/models/request/update_user_request.py +10 -0
  88. pyrestkit/models/response/__init__.py +5 -0
  89. pyrestkit/models/response/create_user_response.py +26 -0
  90. pyrestkit/models/response/get_user_response.py +28 -0
  91. pyrestkit/models/response/user_response.py +12 -0
  92. pyrestkit/pipeline/__init__.py +5 -0
  93. pyrestkit/pipeline/middleware.py +18 -0
  94. pyrestkit/pipeline/middleware_chain.py +11 -0
  95. pyrestkit/pipeline/pipeline.py +27 -0
  96. pyrestkit/pipeline/request_context.py +26 -0
  97. pyrestkit/response/__init__.py +8 -0
  98. pyrestkit/response/framework_response.py +271 -0
  99. pyrestkit/response/response_body.py +124 -0
  100. pyrestkit/retry/__init__.py +5 -0
  101. pyrestkit/retry/backoff.py +32 -0
  102. pyrestkit/retry/retry_handler.py +52 -0
  103. pyrestkit/retry/retry_policy.py +33 -0
  104. pyrestkit/serializers/__init__.py +0 -0
  105. pyrestkit/serializers/response_mapper.py +25 -0
  106. pyrestkit/types/__init__.py +0 -0
  107. pyrestkit/types/model_protocol.py +17 -0
  108. pyrestkit/utils/__init__.py +0 -0
  109. pyrestkit/validators/__init__.py +7 -0
  110. pyrestkit/validators/response_validator.py +57 -0
  111. pyrestkit/validators/schema_validator.py +33 -0
  112. pyrestkit-1.2.0.dist-info/METADATA +741 -0
  113. pyrestkit-1.2.0.dist-info/RECORD +115 -0
  114. pyrestkit-1.2.0.dist-info/WHEEL +5 -0
  115. pyrestkit-1.2.0.dist-info/top_level.txt +1 -0
pyrestkit/__init__.py ADDED
@@ -0,0 +1,35 @@
1
+ """
2
+ PyRestKit
3
+
4
+ Modern Python REST API Automation Toolkit.
5
+ """
6
+
7
+ from pyrestkit.auth.strategies.api_key_auth import ApiKeyAuth
8
+ from pyrestkit.auth.strategies.basic_auth import BasicAuth
9
+ from pyrestkit.auth.strategies.bearer_auth import BearerAuth
10
+ from pyrestkit.clients.user_client import UserClient
11
+ from pyrestkit.config.config import ConfigManager
12
+ from pyrestkit.core.api_client import APIClient
13
+ from pyrestkit.core.session_manager import SessionManager
14
+ from pyrestkit.factories.user_factory import UserFactory
15
+ from pyrestkit.models.request.create_user_request import CreateUserRequest
16
+ from pyrestkit.models.request.update_user_request import UpdateUserRequest
17
+ from pyrestkit.validators.response_validator import ResponseValidator
18
+ from pyrestkit.validators.schema_validator import SchemaValidator
19
+
20
+ __version__ = "1.2.0"
21
+
22
+ __all__ = [
23
+ "APIClient",
24
+ "SessionManager",
25
+ "ConfigManager",
26
+ "BearerAuth",
27
+ "BasicAuth",
28
+ "ApiKeyAuth",
29
+ "UserClient",
30
+ "CreateUserRequest",
31
+ "UpdateUserRequest",
32
+ "ResponseValidator",
33
+ "SchemaValidator",
34
+ "UserFactory",
35
+ ]
@@ -0,0 +1,17 @@
1
+ from pyrestkit.ai.analyzer import FailureAnalyzer, parse_failure_analysis
2
+ from pyrestkit.ai.client import AIClient, available_providers, register_provider
3
+ from pyrestkit.ai.config import AIConfig
4
+ from pyrestkit.ai.models import FailureAnalysis, FailureContext
5
+ from pyrestkit.ai.provider import AIProvider
6
+
7
+ __all__ = [
8
+ "AIClient",
9
+ "AIConfig",
10
+ "AIProvider",
11
+ "FailureAnalysis",
12
+ "FailureAnalyzer",
13
+ "FailureContext",
14
+ "available_providers",
15
+ "parse_failure_analysis",
16
+ "register_provider",
17
+ ]
@@ -0,0 +1,137 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from typing import Any
5
+
6
+ from pyrestkit.ai.client import AIClient
7
+ from pyrestkit.ai.models import FailureAnalysis, FailureContext
8
+ from pyrestkit.ai.utils.prompt_loader import PromptLoader
9
+
10
+ FAILURE_ANALYZER_SYSTEM_PROMPT = (
11
+ "You are an expert Python API automation engineer. "
12
+ "Return concise, actionable failure analysis as JSON only."
13
+ )
14
+
15
+
16
+ class FailureAnalyzer:
17
+ """
18
+ Generates structured analysis and fix suggestions for test failures.
19
+ """
20
+
21
+ def __init__(
22
+ self,
23
+ client: AIClient,
24
+ prompt_loader: PromptLoader | None = None,
25
+ ) -> None:
26
+ self._client = client
27
+ self._prompt_loader = prompt_loader or PromptLoader.default()
28
+
29
+ def build_prompt(
30
+ self,
31
+ context: FailureContext,
32
+ ) -> str:
33
+ return self._prompt_loader.render(
34
+ "failure_analysis.md",
35
+ context=json.dumps(
36
+ context.to_dict(),
37
+ indent=2,
38
+ sort_keys=True,
39
+ ),
40
+ )
41
+
42
+ def analyze(
43
+ self,
44
+ context: FailureContext,
45
+ ) -> FailureAnalysis:
46
+ raw_response = self._client.complete(
47
+ self.build_prompt(context),
48
+ system_prompt=FAILURE_ANALYZER_SYSTEM_PROMPT,
49
+ )
50
+
51
+ return parse_failure_analysis(raw_response)
52
+
53
+
54
+ def parse_failure_analysis(
55
+ raw_response: str,
56
+ ) -> FailureAnalysis:
57
+ try:
58
+ data = json.loads(_extract_json(raw_response))
59
+ except (json.JSONDecodeError, ValueError):
60
+ return FailureAnalysis(
61
+ summary=raw_response.strip(),
62
+ likely_cause="",
63
+ recommended_fix="",
64
+ raw_response=raw_response,
65
+ )
66
+
67
+ if not isinstance(data, dict):
68
+ return FailureAnalysis(
69
+ summary=str(data).strip(),
70
+ likely_cause="",
71
+ recommended_fix="",
72
+ raw_response=raw_response,
73
+ )
74
+
75
+ return FailureAnalysis(
76
+ summary=str(data.get("summary", "")).strip(),
77
+ likely_cause=str(data.get("likely_cause", "")).strip(),
78
+ recommended_fix=str(data.get("recommended_fix", "")).strip(),
79
+ suggested_patch=_optional_text(data.get("suggested_patch")),
80
+ confidence=_confidence(data.get("confidence", 0.0)),
81
+ raw_response=raw_response,
82
+ )
83
+
84
+
85
+ def _extract_json(
86
+ value: str,
87
+ ) -> str:
88
+ text = value.strip()
89
+
90
+ if text.startswith("```"):
91
+ lines = text.splitlines()
92
+
93
+ if lines and lines[0].startswith("```"):
94
+ lines = lines[1:]
95
+
96
+ if lines and lines[-1].startswith("```"):
97
+ lines = lines[:-1]
98
+
99
+ text = "\n".join(lines).strip()
100
+
101
+ if text.startswith("{") and text.endswith("}"):
102
+ return text
103
+
104
+ start = text.find("{")
105
+ end = text.rfind("}")
106
+
107
+ if start == -1 or end == -1 or end <= start:
108
+ raise ValueError("No JSON object found.")
109
+
110
+ return text[start : end + 1]
111
+
112
+
113
+ def _optional_text(
114
+ value: Any,
115
+ ) -> str | None:
116
+ if value is None:
117
+ return None
118
+
119
+ text = str(value).strip()
120
+ return text or None
121
+
122
+
123
+ def _confidence(
124
+ value: Any,
125
+ ) -> float:
126
+ try:
127
+ confidence = float(value)
128
+ except (TypeError, ValueError):
129
+ return 0.0
130
+
131
+ if confidence < 0.0:
132
+ return 0.0
133
+
134
+ if confidence > 1.0:
135
+ return 1.0
136
+
137
+ return confidence
pyrestkit/ai/client.py ADDED
@@ -0,0 +1,101 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+
5
+ from pyrestkit.ai.config import AIConfig
6
+ from pyrestkit.ai.exceptions import AIConfigurationError
7
+ from pyrestkit.ai.provider import AIProvider
8
+ from pyrestkit.ai.providers import (
9
+ AnthropicProvider,
10
+ AzureOpenAIProvider,
11
+ BedrockProvider,
12
+ CohereProvider,
13
+ GeminiProvider,
14
+ GroqProvider,
15
+ MistralProvider,
16
+ OllamaProvider,
17
+ OpenAIResponsesProvider,
18
+ )
19
+
20
+ ProviderFactory = Callable[[], AIProvider]
21
+
22
+ _PROVIDER_FACTORIES: dict[str, ProviderFactory] = {
23
+ "openai": OpenAIResponsesProvider,
24
+ "ollama": OllamaProvider,
25
+ "anthropic": AnthropicProvider,
26
+ "gemini": GeminiProvider,
27
+ "azure-openai": AzureOpenAIProvider,
28
+ "azure_openai": AzureOpenAIProvider,
29
+ "cohere": CohereProvider,
30
+ "mistral": MistralProvider,
31
+ "groq": GroqProvider,
32
+ "bedrock": BedrockProvider,
33
+ }
34
+
35
+
36
+ class AIClient:
37
+ """
38
+ Small facade that keeps framework code independent from provider details.
39
+ """
40
+
41
+ def __init__(
42
+ self,
43
+ config: AIConfig,
44
+ provider: AIProvider | None = None,
45
+ ) -> None:
46
+ self._config = config
47
+ self._provider = provider or create_provider(config.provider)
48
+
49
+ @property
50
+ def config(self) -> AIConfig:
51
+ return self._config
52
+
53
+ def complete(
54
+ self,
55
+ prompt: str,
56
+ *,
57
+ system_prompt: str | None = None,
58
+ ) -> str:
59
+ if not prompt.strip():
60
+ raise ValueError("Prompt cannot be empty.")
61
+
62
+ return self._provider.complete(
63
+ prompt,
64
+ config=self._config,
65
+ system_prompt=system_prompt,
66
+ )
67
+
68
+
69
+ def create_provider(
70
+ provider_name: str,
71
+ ) -> AIProvider:
72
+ provider = provider_name.strip().lower()
73
+
74
+ if not provider:
75
+ raise AIConfigurationError("AI provider name cannot be empty.")
76
+
77
+ factory = _PROVIDER_FACTORIES.get(provider)
78
+
79
+ if factory is None:
80
+ raise AIConfigurationError(
81
+ f"No AI provider registered for '{provider_name}'. "
82
+ "Install the provider package or register a custom provider."
83
+ )
84
+
85
+ return factory()
86
+
87
+
88
+ def register_provider(
89
+ provider_name: str,
90
+ factory: ProviderFactory,
91
+ ) -> None:
92
+ provider = provider_name.strip().lower()
93
+
94
+ if not provider:
95
+ raise AIConfigurationError("AI provider name cannot be empty.")
96
+
97
+ _PROVIDER_FACTORIES[provider] = factory
98
+
99
+
100
+ def available_providers() -> tuple[str, ...]:
101
+ return tuple(sorted(_PROVIDER_FACTORIES))
@@ -0,0 +1,5 @@
1
+ from pyrestkit.ai.config.ai_config import AIConfig
2
+
3
+ __all__ = [
4
+ "AIConfig",
5
+ ]
@@ -0,0 +1,200 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from collections.abc import Mapping
5
+ from dataclasses import dataclass, field
6
+ from typing import Any
7
+
8
+
9
+ @dataclass(frozen=True, slots=True)
10
+ class AIConfig:
11
+ """
12
+ Provider-agnostic configuration for AI integrations.
13
+ """
14
+
15
+ provider: str
16
+ model: str
17
+
18
+ api_key: str | None = None
19
+ base_url: str | None = None
20
+
21
+ temperature: float = 0.2
22
+ max_tokens: int | None = None
23
+
24
+ timeout: int = 60
25
+
26
+ organization: str | None = None
27
+
28
+ headers: Mapping[str, str] = field(default_factory=dict)
29
+
30
+ def __post_init__(self) -> None:
31
+ provider = self.provider.strip().lower()
32
+ model = self.model.strip()
33
+
34
+ object.__setattr__(self, "provider", provider)
35
+ object.__setattr__(self, "model", model)
36
+ object.__setattr__(self, "api_key", _blank_to_none(self.api_key))
37
+ object.__setattr__(self, "base_url", _blank_to_none(self.base_url))
38
+ object.__setattr__(self, "organization", _blank_to_none(self.organization))
39
+ object.__setattr__(self, "headers", dict(self.headers))
40
+
41
+ if not provider:
42
+ raise ValueError("Provider cannot be empty.")
43
+
44
+ if not model:
45
+ raise ValueError("Model cannot be empty.")
46
+
47
+ if not 0.0 <= self.temperature <= 2.0:
48
+ raise ValueError("Temperature must be between 0.0 and 2.0.")
49
+
50
+ if self.max_tokens is not None and self.max_tokens <= 0:
51
+ raise ValueError("max_tokens must be greater than zero.")
52
+
53
+ if self.timeout <= 0:
54
+ raise ValueError("timeout must be greater than zero.")
55
+
56
+ @classmethod
57
+ def from_mapping(
58
+ cls,
59
+ values: Mapping[str, Any],
60
+ ) -> AIConfig:
61
+ """
62
+ Build AI configuration from a flat mapping or an ``ai`` section.
63
+ """
64
+
65
+ if not isinstance(values, Mapping):
66
+ raise ValueError("AI configuration must be a mapping.")
67
+
68
+ source = values.get("ai", values)
69
+
70
+ if not isinstance(source, Mapping):
71
+ raise ValueError("AI configuration must be a mapping.")
72
+
73
+ provider = _required_str(source, "provider")
74
+ model = _required_str(source, "model")
75
+ timeout = _optional_int(source, "timeout", default=60)
76
+
77
+ if timeout is None:
78
+ raise ValueError("timeout is required.")
79
+
80
+ return cls(
81
+ provider=provider,
82
+ model=model,
83
+ api_key=_optional_str(source, "api_key"),
84
+ base_url=_optional_str(source, "base_url"),
85
+ temperature=_optional_float(source, "temperature", default=0.2),
86
+ max_tokens=_optional_int(source, "max_tokens"),
87
+ timeout=timeout,
88
+ organization=_optional_str(source, "organization"),
89
+ headers=_optional_headers(source),
90
+ )
91
+
92
+ @classmethod
93
+ def from_env(
94
+ cls,
95
+ prefix: str = "PYRESTKIT_AI_",
96
+ ) -> AIConfig:
97
+ """
98
+ Build AI configuration from environment variables.
99
+
100
+ Expected variables:
101
+
102
+ - PYRESTKIT_AI_PROVIDER
103
+ - PYRESTKIT_AI_MODEL
104
+ - PYRESTKIT_AI_API_KEY
105
+ - PYRESTKIT_AI_BASE_URL
106
+ - PYRESTKIT_AI_TEMPERATURE
107
+ - PYRESTKIT_AI_MAX_TOKENS
108
+ - PYRESTKIT_AI_TIMEOUT
109
+ - PYRESTKIT_AI_ORGANIZATION
110
+ """
111
+
112
+ model = os.getenv(f"{prefix}MODEL")
113
+
114
+ if model is None:
115
+ raise ValueError(f"{prefix}MODEL is required.")
116
+
117
+ mapping: dict[str, Any] = {
118
+ "provider": os.getenv(f"{prefix}PROVIDER", "openai"),
119
+ "model": model,
120
+ "api_key": os.getenv(f"{prefix}API_KEY"),
121
+ "base_url": os.getenv(f"{prefix}BASE_URL"),
122
+ "temperature": os.getenv(f"{prefix}TEMPERATURE", "0.2"),
123
+ "max_tokens": os.getenv(f"{prefix}MAX_TOKENS"),
124
+ "timeout": os.getenv(f"{prefix}TIMEOUT", "60"),
125
+ "organization": os.getenv(f"{prefix}ORGANIZATION"),
126
+ }
127
+
128
+ return cls.from_mapping(mapping)
129
+
130
+
131
+ def _blank_to_none(value: str | None) -> str | None:
132
+ if value is None:
133
+ return None
134
+
135
+ value = value.strip()
136
+ return value or None
137
+
138
+
139
+ def _required_str(
140
+ values: Mapping[str, Any],
141
+ key: str,
142
+ ) -> str:
143
+ value = values.get(key)
144
+
145
+ if value is None:
146
+ raise ValueError(f"{key} is required.")
147
+
148
+ return str(value)
149
+
150
+
151
+ def _optional_str(
152
+ values: Mapping[str, Any],
153
+ key: str,
154
+ ) -> str | None:
155
+ value = values.get(key)
156
+
157
+ if value is None:
158
+ return None
159
+
160
+ return str(value)
161
+
162
+
163
+ def _optional_float(
164
+ values: Mapping[str, Any],
165
+ key: str,
166
+ default: float,
167
+ ) -> float:
168
+ value = values.get(key, default)
169
+
170
+ if value is None or value == "":
171
+ return default
172
+
173
+ return float(value)
174
+
175
+
176
+ def _optional_int(
177
+ values: Mapping[str, Any],
178
+ key: str,
179
+ default: int | None = None,
180
+ ) -> int | None:
181
+ value = values.get(key, default)
182
+
183
+ if value is None or value == "":
184
+ return default
185
+
186
+ return int(value)
187
+
188
+
189
+ def _optional_headers(
190
+ values: Mapping[str, Any],
191
+ ) -> Mapping[str, str]:
192
+ headers = values.get("headers", {})
193
+
194
+ if headers is None:
195
+ return {}
196
+
197
+ if not isinstance(headers, Mapping):
198
+ raise ValueError("headers must be a mapping.")
199
+
200
+ return {str(key): str(value) for key, value in headers.items()}
@@ -0,0 +1,22 @@
1
+ class AIError(Exception):
2
+ """
3
+ Base exception for AI integration failures.
4
+ """
5
+
6
+
7
+ class AIConfigurationError(AIError):
8
+ """
9
+ Raised when AI provider configuration is missing or invalid.
10
+ """
11
+
12
+
13
+ class AIProviderError(AIError):
14
+ """
15
+ Raised when an AI provider request fails.
16
+ """
17
+
18
+
19
+ class AIResponseParseError(AIError):
20
+ """
21
+ Raised when an AI response cannot be parsed into the expected shape.
22
+ """
File without changes
pyrestkit/ai/models.py ADDED
@@ -0,0 +1,44 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping
4
+ from dataclasses import asdict, dataclass, field
5
+ from typing import Any
6
+
7
+
8
+ @dataclass(frozen=True, slots=True)
9
+ class FailureContext:
10
+ """
11
+ Normalized failure details that can be sent to an AI analyzer.
12
+ """
13
+
14
+ test_name: str
15
+ file_path: str | None = None
16
+ line_number: int | None = None
17
+ error_type: str | None = None
18
+ error_message: str | None = None
19
+ traceback: str | None = None
20
+ assertion: str | None = None
21
+ expected: str | None = None
22
+ actual: str | None = None
23
+ request_method: str | None = None
24
+ request_url: str | None = None
25
+ response_status: int | None = None
26
+ response_body: str | None = None
27
+ metadata: Mapping[str, Any] = field(default_factory=dict)
28
+
29
+ def to_dict(self) -> dict[str, Any]:
30
+ return asdict(self)
31
+
32
+
33
+ @dataclass(frozen=True, slots=True)
34
+ class FailureAnalysis:
35
+ """
36
+ Structured AI output for a failing automation test.
37
+ """
38
+
39
+ summary: str
40
+ likely_cause: str
41
+ recommended_fix: str
42
+ suggested_patch: str | None = None
43
+ confidence: float = 0.0
44
+ raw_response: str | None = None
File without changes
@@ -0,0 +1,21 @@
1
+ Analyze this API automation failure and identify the smallest useful fix.
2
+
3
+ Return only a JSON object with this shape:
4
+
5
+ {
6
+ "summary": "one sentence failure summary",
7
+ "likely_cause": "most likely root cause",
8
+ "recommended_fix": "specific next action",
9
+ "suggested_patch": "unified diff if a safe patch is obvious, otherwise null",
10
+ "confidence": 0.0
11
+ }
12
+
13
+ Rules:
14
+ - Do not invent endpoint behavior that is not present in the context.
15
+ - Prefer fixing test data, assertions, schemas, or client code only when the context supports it.
16
+ - Keep suggested patches minimal and in unified diff format.
17
+ - Do not include secrets or credentials in the response.
18
+
19
+ Failure context:
20
+
21
+ $context
@@ -0,0 +1,58 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Protocol
5
+
6
+ from pyrestkit.ai.config import AIConfig
7
+
8
+
9
+ class AIProvider(Protocol):
10
+ """
11
+ Contract implemented by concrete AI providers.
12
+ """
13
+
14
+ def complete(
15
+ self,
16
+ prompt: str,
17
+ *,
18
+ config: AIConfig,
19
+ system_prompt: str | None = None,
20
+ ) -> str:
21
+ """
22
+ Return a text completion for the given prompt.
23
+ """
24
+ ...
25
+
26
+
27
+ @dataclass(frozen=True, slots=True)
28
+ class AIProviderCall:
29
+ prompt: str
30
+ system_prompt: str | None
31
+ config: AIConfig
32
+
33
+
34
+ @dataclass(slots=True)
35
+ class StaticAIProvider:
36
+ """
37
+ Deterministic provider for tests and offline demos.
38
+ """
39
+
40
+ response: str
41
+ calls: list[AIProviderCall] = field(default_factory=list)
42
+
43
+ def complete(
44
+ self,
45
+ prompt: str,
46
+ *,
47
+ config: AIConfig,
48
+ system_prompt: str | None = None,
49
+ ) -> str:
50
+ self.calls.append(
51
+ AIProviderCall(
52
+ prompt=prompt,
53
+ system_prompt=system_prompt,
54
+ config=config,
55
+ )
56
+ )
57
+
58
+ return self.response
@@ -0,0 +1,21 @@
1
+ from pyrestkit.ai.providers.anthropic import AnthropicProvider
2
+ from pyrestkit.ai.providers.azure_openai import AzureOpenAIProvider
3
+ from pyrestkit.ai.providers.bedrock import BedrockProvider
4
+ from pyrestkit.ai.providers.cohere import CohereProvider
5
+ from pyrestkit.ai.providers.gemini import GeminiProvider
6
+ from pyrestkit.ai.providers.groq import GroqProvider
7
+ from pyrestkit.ai.providers.mistral import MistralProvider
8
+ from pyrestkit.ai.providers.ollama import OllamaProvider
9
+ from pyrestkit.ai.providers.openai import OpenAIResponsesProvider
10
+
11
+ __all__ = [
12
+ "AnthropicProvider",
13
+ "AzureOpenAIProvider",
14
+ "BedrockProvider",
15
+ "CohereProvider",
16
+ "GeminiProvider",
17
+ "GroqProvider",
18
+ "MistralProvider",
19
+ "OllamaProvider",
20
+ "OpenAIResponsesProvider",
21
+ ]