codeframe-ai 0.9.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.
- codeframe/__init__.py +11 -0
- codeframe/__main__.py +20 -0
- codeframe/adapters/__init__.py +5 -0
- codeframe/adapters/e2b/__init__.py +13 -0
- codeframe/adapters/e2b/adapter.py +342 -0
- codeframe/adapters/e2b/budget.py +71 -0
- codeframe/adapters/e2b/credential_scanner.py +134 -0
- codeframe/adapters/llm/__init__.py +92 -0
- codeframe/adapters/llm/anthropic.py +414 -0
- codeframe/adapters/llm/base.py +444 -0
- codeframe/adapters/llm/mock.py +281 -0
- codeframe/adapters/llm/openai.py +483 -0
- codeframe/agents/__init__.py +8 -0
- codeframe/agents/dependency_resolver.py +714 -0
- codeframe/auth/__init__.py +16 -0
- codeframe/auth/api_key_router.py +238 -0
- codeframe/auth/api_keys.py +156 -0
- codeframe/auth/dependencies.py +358 -0
- codeframe/auth/manager.py +178 -0
- codeframe/auth/models.py +30 -0
- codeframe/auth/router.py +93 -0
- codeframe/auth/schemas.py +15 -0
- codeframe/auth/scopes.py +53 -0
- codeframe/cli/__init__.py +12 -0
- codeframe/cli/__main__.py +20 -0
- codeframe/cli/api_client.py +275 -0
- codeframe/cli/app.py +5688 -0
- codeframe/cli/auth.py +122 -0
- codeframe/cli/auth_commands.py +958 -0
- codeframe/cli/commands/__init__.py +5 -0
- codeframe/cli/config_commands.py +79 -0
- codeframe/cli/dashboard_commands.py +67 -0
- codeframe/cli/engines_commands.py +205 -0
- codeframe/cli/env_commands.py +409 -0
- codeframe/cli/helpers.py +56 -0
- codeframe/cli/hooks_commands.py +208 -0
- codeframe/cli/import_commands.py +129 -0
- codeframe/cli/pr_commands.py +549 -0
- codeframe/cli/proof_commands.py +415 -0
- codeframe/cli/stats_commands.py +311 -0
- codeframe/cli/telemetry_runtime.py +153 -0
- codeframe/cli/validators.py +123 -0
- codeframe/config/rate_limits.py +165 -0
- codeframe/core/__init__.py +15 -0
- codeframe/core/adapters/__init__.py +43 -0
- codeframe/core/adapters/agent_adapter.py +114 -0
- codeframe/core/adapters/builtin.py +326 -0
- codeframe/core/adapters/claude_code.py +62 -0
- codeframe/core/adapters/codex.py +393 -0
- codeframe/core/adapters/git_utils.py +40 -0
- codeframe/core/adapters/kilocode.py +126 -0
- codeframe/core/adapters/opencode.py +48 -0
- codeframe/core/adapters/streaming_chat.py +483 -0
- codeframe/core/adapters/subprocess_adapter.py +213 -0
- codeframe/core/adapters/verification_wrapper.py +269 -0
- codeframe/core/agent.py +2183 -0
- codeframe/core/agents_config.py +569 -0
- codeframe/core/api_key_service.py +211 -0
- codeframe/core/artifacts.py +428 -0
- codeframe/core/blocker_detection.py +218 -0
- codeframe/core/blockers.py +433 -0
- codeframe/core/checkpoints.py +481 -0
- codeframe/core/conductor.py +2255 -0
- codeframe/core/config.py +827 -0
- codeframe/core/config_watcher.py +268 -0
- codeframe/core/context.py +542 -0
- codeframe/core/context_packager.py +234 -0
- codeframe/core/credentials.py +735 -0
- codeframe/core/dependency_analyzer.py +229 -0
- codeframe/core/dependency_graph.py +290 -0
- codeframe/core/diagnostic_agent.py +712 -0
- codeframe/core/diagnostics.py +616 -0
- codeframe/core/editor.py +556 -0
- codeframe/core/engine_registry.py +256 -0
- codeframe/core/engine_stats.py +231 -0
- codeframe/core/environment.py +697 -0
- codeframe/core/events.py +375 -0
- codeframe/core/executor.py +1005 -0
- codeframe/core/fix_tracker.py +480 -0
- codeframe/core/gates.py +1322 -0
- codeframe/core/git.py +477 -0
- codeframe/core/github_connect_service.py +178 -0
- codeframe/core/github_integration_config.py +118 -0
- codeframe/core/github_issues_service.py +449 -0
- codeframe/core/hooks.py +184 -0
- codeframe/core/importers/__init__.py +1 -0
- codeframe/core/importers/ralph.py +540 -0
- codeframe/core/installer.py +650 -0
- codeframe/core/models.py +1026 -0
- codeframe/core/notifications_config.py +183 -0
- codeframe/core/planner.py +437 -0
- codeframe/core/prd.py +670 -0
- codeframe/core/prd_discovery.py +1118 -0
- codeframe/core/prd_stress_test.py +499 -0
- codeframe/core/progress.py +126 -0
- codeframe/core/proof/__init__.py +34 -0
- codeframe/core/proof/capture.py +79 -0
- codeframe/core/proof/evidence.py +56 -0
- codeframe/core/proof/ledger.py +574 -0
- codeframe/core/proof/models.py +162 -0
- codeframe/core/proof/obligations.py +103 -0
- codeframe/core/proof/runner.py +233 -0
- codeframe/core/proof/scope.py +81 -0
- codeframe/core/proof/stubs.py +156 -0
- codeframe/core/quick_fixes.py +558 -0
- codeframe/core/react_agent.py +1650 -0
- codeframe/core/reconciliation.py +183 -0
- codeframe/core/replay.py +788 -0
- codeframe/core/review.py +285 -0
- codeframe/core/runtime.py +1134 -0
- codeframe/core/sandbox/__init__.py +27 -0
- codeframe/core/sandbox/context.py +98 -0
- codeframe/core/sandbox/worktree.py +20 -0
- codeframe/core/schedule.py +396 -0
- codeframe/core/stall_detector.py +71 -0
- codeframe/core/stall_monitor.py +134 -0
- codeframe/core/state_machine.py +121 -0
- codeframe/core/streaming.py +502 -0
- codeframe/core/task_tree.py +400 -0
- codeframe/core/tasks.py +1022 -0
- codeframe/core/telemetry.py +232 -0
- codeframe/core/templates.py +221 -0
- codeframe/core/tools.py +942 -0
- codeframe/core/workspace.py +887 -0
- codeframe/core/worktrees.py +276 -0
- codeframe/git/__init__.py +5 -0
- codeframe/git/github_integration.py +505 -0
- codeframe/lib/__init__.py +0 -0
- codeframe/lib/audit_logger.py +248 -0
- codeframe/lib/metrics_tracker.py +800 -0
- codeframe/lib/quality/__init__.py +7 -0
- codeframe/lib/quality/complexity_analyzer.py +316 -0
- codeframe/lib/quality/owasp_patterns.py +284 -0
- codeframe/lib/quality/security_scanner.py +250 -0
- codeframe/lib/rate_limiter.py +312 -0
- codeframe/notifications/__init__.py +0 -0
- codeframe/notifications/webhook.py +380 -0
- codeframe/planning/__init__.py +30 -0
- codeframe/planning/issue_generator.py +219 -0
- codeframe/planning/prd_template_functions.py +137 -0
- codeframe/planning/prd_templates.py +975 -0
- codeframe/planning/task_scheduler.py +511 -0
- codeframe/planning/task_templates.py +533 -0
- codeframe/platform_store/__init__.py +5 -0
- codeframe/platform_store/database.py +277 -0
- codeframe/platform_store/repositories/__init__.py +24 -0
- codeframe/platform_store/repositories/api_key_repository.py +245 -0
- codeframe/platform_store/repositories/audit_repository.py +67 -0
- codeframe/platform_store/repositories/base.py +295 -0
- codeframe/platform_store/repositories/interactive_sessions.py +165 -0
- codeframe/platform_store/repositories/token_repository.py +598 -0
- codeframe/platform_store/repositories/workspace_registry_repository.py +175 -0
- codeframe/platform_store/schema_manager.py +321 -0
- codeframe/templates/AGENTS.md.default +94 -0
- codeframe/tui/__init__.py +5 -0
- codeframe/tui/app.py +256 -0
- codeframe/tui/data_service.py +103 -0
- codeframe/ui/__init__.py +0 -0
- codeframe/ui/dependencies.py +103 -0
- codeframe/ui/models.py +999 -0
- codeframe/ui/response_models.py +201 -0
- codeframe/ui/routers/__init__.py +5 -0
- codeframe/ui/routers/_helpers.py +29 -0
- codeframe/ui/routers/batches_v2.py +315 -0
- codeframe/ui/routers/blockers_v2.py +320 -0
- codeframe/ui/routers/checkpoints_v2.py +310 -0
- codeframe/ui/routers/costs_v2.py +322 -0
- codeframe/ui/routers/diagnose_v2.py +225 -0
- codeframe/ui/routers/discovery_v2.py +417 -0
- codeframe/ui/routers/environment_v2.py +284 -0
- codeframe/ui/routers/events_v2.py +75 -0
- codeframe/ui/routers/gates_v2.py +166 -0
- codeframe/ui/routers/git_v2.py +284 -0
- codeframe/ui/routers/github_integrations_v2.py +532 -0
- codeframe/ui/routers/interactive_sessions_v2.py +238 -0
- codeframe/ui/routers/pr_v2.py +709 -0
- codeframe/ui/routers/prd_v2.py +695 -0
- codeframe/ui/routers/proof_v2.py +755 -0
- codeframe/ui/routers/review_v2.py +360 -0
- codeframe/ui/routers/schedule_v2.py +214 -0
- codeframe/ui/routers/session_chat_ws.py +354 -0
- codeframe/ui/routers/settings_v2.py +562 -0
- codeframe/ui/routers/streaming_v2.py +155 -0
- codeframe/ui/routers/tasks_v2.py +1098 -0
- codeframe/ui/routers/templates_v2.py +232 -0
- codeframe/ui/routers/terminal_ws.py +267 -0
- codeframe/ui/routers/workspace_v2.py +527 -0
- codeframe/ui/server.py +568 -0
- codeframe/ui/shared.py +241 -0
- codeframe/workspace/__init__.py +5 -0
- codeframe/workspace/manager.py +249 -0
- codeframe_ai-0.9.0.dist-info/METADATA +517 -0
- codeframe_ai-0.9.0.dist-info/RECORD +197 -0
- codeframe_ai-0.9.0.dist-info/WHEEL +5 -0
- codeframe_ai-0.9.0.dist-info/entry_points.txt +3 -0
- codeframe_ai-0.9.0.dist-info/licenses/LICENSE +661 -0
- codeframe_ai-0.9.0.dist-info/top_level.txt +1 -0
codeframe/auth/scopes.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Scope hierarchy and permission checking for API key authentication.
|
|
2
|
+
|
|
3
|
+
Defines the permission model:
|
|
4
|
+
- read: Read-only access to resources
|
|
5
|
+
- write: Read and write access (implies read)
|
|
6
|
+
- admin: Full access including admin operations (implies all)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Dict, List
|
|
10
|
+
|
|
11
|
+
from codeframe.auth.api_keys import SCOPE_READ, SCOPE_WRITE, SCOPE_ADMIN
|
|
12
|
+
|
|
13
|
+
# Scope hierarchy: each scope grants the permissions listed
|
|
14
|
+
SCOPE_HIERARCHY: Dict[str, List[str]] = {
|
|
15
|
+
SCOPE_ADMIN: [SCOPE_READ, SCOPE_WRITE, SCOPE_ADMIN],
|
|
16
|
+
SCOPE_WRITE: [SCOPE_READ, SCOPE_WRITE],
|
|
17
|
+
SCOPE_READ: [SCOPE_READ],
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def has_scope(principal: dict, required_scope: str) -> bool:
|
|
22
|
+
"""Check if a principal has the required scope.
|
|
23
|
+
|
|
24
|
+
Uses scope hierarchy: admin grants all, write grants read.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
principal: Authentication dict with 'scopes' list
|
|
28
|
+
required_scope: The scope to check for
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
True if principal has the required scope (directly or via hierarchy)
|
|
32
|
+
"""
|
|
33
|
+
user_scopes = principal.get("scopes", [])
|
|
34
|
+
|
|
35
|
+
# Check each user scope and its implied permissions
|
|
36
|
+
for scope in user_scopes:
|
|
37
|
+
granted_permissions = SCOPE_HIERARCHY.get(scope, [scope])
|
|
38
|
+
if required_scope in granted_permissions:
|
|
39
|
+
return True
|
|
40
|
+
|
|
41
|
+
return False
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_scope_permissions(scope: str) -> List[str]:
|
|
45
|
+
"""Get all permissions granted by a scope.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
scope: The scope to check
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
List of all permissions granted by this scope
|
|
52
|
+
"""
|
|
53
|
+
return SCOPE_HIERARCHY.get(scope, [scope])
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Command-line interface for CodeFRAME.
|
|
2
|
+
|
|
3
|
+
The Typer entry point lives in :mod:`codeframe.cli.app` and is exposed as the
|
|
4
|
+
``codeframe`` / ``cf`` console scripts (see ``pyproject.toml``):
|
|
5
|
+
|
|
6
|
+
codeframe = "codeframe.cli.app:app"
|
|
7
|
+
cf = "codeframe.cli.app:app"
|
|
8
|
+
|
|
9
|
+
This package ``__init__`` is intentionally empty so that importing
|
|
10
|
+
``codeframe.cli.app`` does not drag in unrelated command modules as a side
|
|
11
|
+
effect.
|
|
12
|
+
"""
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Entry point for `python -m codeframe.cli` invocation.
|
|
2
|
+
|
|
3
|
+
This module enables running the CLI via:
|
|
4
|
+
python -m codeframe.cli [command] [options]
|
|
5
|
+
|
|
6
|
+
The help text will correctly show 'codeframe' as the command name.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def main():
|
|
11
|
+
"""Run the CLI with proper program name."""
|
|
12
|
+
from codeframe.cli.app import main as app_main
|
|
13
|
+
|
|
14
|
+
# The telemetry-aware wrapper invokes the app with prog_name="codeframe",
|
|
15
|
+
# so the usage line shows the right command name here too.
|
|
16
|
+
app_main()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
if __name__ == "__main__":
|
|
20
|
+
main()
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
"""CLI API client module - HTTP client with authentication.
|
|
2
|
+
|
|
3
|
+
This module provides:
|
|
4
|
+
- APIClient class for making authenticated HTTP requests
|
|
5
|
+
- Automatic token injection in Authorization header
|
|
6
|
+
- Retry logic for transient failures
|
|
7
|
+
- User-friendly error messages
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
from codeframe.cli.api_client import APIClient
|
|
11
|
+
|
|
12
|
+
client = APIClient() # Auto-loads token from storage
|
|
13
|
+
projects = client.get("/api/projects")
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import logging
|
|
18
|
+
import os
|
|
19
|
+
import time
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
import requests
|
|
23
|
+
|
|
24
|
+
from codeframe.cli.auth import get_token
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class APIError(Exception):
|
|
30
|
+
"""Base exception for API errors."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, message: str, status_code: int | None = None, detail: str | None = None):
|
|
33
|
+
self.status_code = status_code
|
|
34
|
+
self.detail = detail
|
|
35
|
+
super().__init__(message)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class AuthenticationError(APIError):
|
|
39
|
+
"""Exception for authentication failures (401, 403)."""
|
|
40
|
+
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_api_base_url() -> str:
|
|
45
|
+
"""Get the API base URL.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
API base URL from CODEFRAME_API_URL env var, or default localhost:8080
|
|
49
|
+
"""
|
|
50
|
+
url = os.environ.get("CODEFRAME_API_URL", "http://localhost:8080")
|
|
51
|
+
return url.rstrip("/")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class APIClient:
|
|
55
|
+
"""HTTP client for CodeFRAME API with authentication.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
base_url: API base URL. Defaults to CODEFRAME_API_URL env var or localhost:8080
|
|
59
|
+
token: JWT token. If not provided, loads from storage
|
|
60
|
+
max_retries: Maximum number of retries for transient failures (default: 3)
|
|
61
|
+
timeout: Request timeout in seconds (default: 30)
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
def __init__(
|
|
65
|
+
self,
|
|
66
|
+
base_url: str | None = None,
|
|
67
|
+
token: str | None = None,
|
|
68
|
+
max_retries: int = 3,
|
|
69
|
+
timeout: int = 30,
|
|
70
|
+
):
|
|
71
|
+
self.base_url = base_url or get_api_base_url()
|
|
72
|
+
self.token = token if token is not None else get_token()
|
|
73
|
+
self.max_retries = max_retries
|
|
74
|
+
self.timeout = timeout
|
|
75
|
+
|
|
76
|
+
def _get_headers(self) -> dict[str, str]:
|
|
77
|
+
"""Get HTTP headers including auth token.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Headers dict with Content-Type and optional Authorization
|
|
81
|
+
"""
|
|
82
|
+
headers = {"Content-Type": "application/json"}
|
|
83
|
+
|
|
84
|
+
if self.token:
|
|
85
|
+
headers["Authorization"] = f"Bearer {self.token}"
|
|
86
|
+
|
|
87
|
+
return headers
|
|
88
|
+
|
|
89
|
+
def _make_url(self, endpoint: str) -> str:
|
|
90
|
+
"""Build full URL from endpoint.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
endpoint: API endpoint (e.g., "/api/projects")
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Full URL
|
|
97
|
+
"""
|
|
98
|
+
if endpoint.startswith("http"):
|
|
99
|
+
return endpoint
|
|
100
|
+
return f"{self.base_url}{endpoint}"
|
|
101
|
+
|
|
102
|
+
def _handle_response(self, response: requests.Response) -> Any:
|
|
103
|
+
"""Handle HTTP response, raising appropriate exceptions.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
response: requests Response object
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
Parsed JSON response, or None for 204
|
|
110
|
+
|
|
111
|
+
Raises:
|
|
112
|
+
AuthenticationError: For 401/403 responses
|
|
113
|
+
APIError: For other error responses
|
|
114
|
+
"""
|
|
115
|
+
status = response.status_code
|
|
116
|
+
|
|
117
|
+
# Success - return JSON or None
|
|
118
|
+
if 200 <= status < 300:
|
|
119
|
+
if status == 204 or not response.text:
|
|
120
|
+
return None
|
|
121
|
+
try:
|
|
122
|
+
return response.json()
|
|
123
|
+
except json.JSONDecodeError:
|
|
124
|
+
return response.text
|
|
125
|
+
|
|
126
|
+
# Authentication errors
|
|
127
|
+
if status == 401:
|
|
128
|
+
raise AuthenticationError(
|
|
129
|
+
"Authentication failed. Please log in with: codeframe auth login",
|
|
130
|
+
status_code=status,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
if status == 403:
|
|
134
|
+
raise AuthenticationError(
|
|
135
|
+
"Access denied. You don't have permission for this resource.",
|
|
136
|
+
status_code=status,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# Parse error detail from response
|
|
140
|
+
detail = None
|
|
141
|
+
try:
|
|
142
|
+
error_data = response.json()
|
|
143
|
+
detail = error_data.get("detail", str(error_data))
|
|
144
|
+
except (json.JSONDecodeError, TypeError):
|
|
145
|
+
detail = response.text or f"HTTP {status}"
|
|
146
|
+
|
|
147
|
+
# Client errors (4xx)
|
|
148
|
+
if 400 <= status < 500:
|
|
149
|
+
raise APIError(
|
|
150
|
+
f"Request failed: {detail}",
|
|
151
|
+
status_code=status,
|
|
152
|
+
detail=detail,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# Server errors (5xx)
|
|
156
|
+
raise APIError(
|
|
157
|
+
f"Server error ({status}): {detail}",
|
|
158
|
+
status_code=status,
|
|
159
|
+
detail=detail,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
def _request_with_retry(
|
|
163
|
+
self,
|
|
164
|
+
method: str,
|
|
165
|
+
endpoint: str,
|
|
166
|
+
**kwargs,
|
|
167
|
+
) -> Any:
|
|
168
|
+
"""Make HTTP request with retry logic.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
method: HTTP method (GET, POST, etc.)
|
|
172
|
+
endpoint: API endpoint
|
|
173
|
+
**kwargs: Additional arguments for requests
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
Parsed response data
|
|
177
|
+
|
|
178
|
+
Raises:
|
|
179
|
+
APIError: After all retries exhausted
|
|
180
|
+
"""
|
|
181
|
+
url = self._make_url(endpoint)
|
|
182
|
+
headers = self._get_headers()
|
|
183
|
+
|
|
184
|
+
for attempt in range(self.max_retries):
|
|
185
|
+
try:
|
|
186
|
+
response = requests.request(
|
|
187
|
+
method=method,
|
|
188
|
+
url=url,
|
|
189
|
+
headers=headers,
|
|
190
|
+
timeout=self.timeout,
|
|
191
|
+
**kwargs,
|
|
192
|
+
)
|
|
193
|
+
return self._handle_response(response)
|
|
194
|
+
|
|
195
|
+
except requests.ConnectionError as e:
|
|
196
|
+
logger.warning(f"Connection error (attempt {attempt + 1}/{self.max_retries}): {e}")
|
|
197
|
+
|
|
198
|
+
if attempt < self.max_retries - 1:
|
|
199
|
+
# Exponential backoff: 1s, 2s, 4s, ...
|
|
200
|
+
sleep_time = 2 ** attempt
|
|
201
|
+
time.sleep(sleep_time)
|
|
202
|
+
continue
|
|
203
|
+
|
|
204
|
+
except requests.Timeout as e:
|
|
205
|
+
logger.warning(f"Request timeout (attempt {attempt + 1}/{self.max_retries}): {e}")
|
|
206
|
+
|
|
207
|
+
if attempt < self.max_retries - 1:
|
|
208
|
+
time.sleep(2 ** attempt)
|
|
209
|
+
continue
|
|
210
|
+
|
|
211
|
+
# All retries exhausted
|
|
212
|
+
raise APIError(
|
|
213
|
+
f"Connection error: Unable to connect to {self.base_url}. "
|
|
214
|
+
"Please check the server is running and try again.",
|
|
215
|
+
status_code=None,
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
def get(self, endpoint: str, params: dict | None = None) -> Any:
|
|
219
|
+
"""Make GET request.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
endpoint: API endpoint (e.g., "/api/projects")
|
|
223
|
+
params: Query parameters
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
Parsed JSON response
|
|
227
|
+
"""
|
|
228
|
+
return self._request_with_retry("GET", endpoint, params=params)
|
|
229
|
+
|
|
230
|
+
def post(self, endpoint: str, data: dict | None = None) -> Any:
|
|
231
|
+
"""Make POST request.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
endpoint: API endpoint
|
|
235
|
+
data: Request body (will be sent as JSON)
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
Parsed JSON response
|
|
239
|
+
"""
|
|
240
|
+
return self._request_with_retry("POST", endpoint, json=data)
|
|
241
|
+
|
|
242
|
+
def put(self, endpoint: str, data: dict | None = None) -> Any:
|
|
243
|
+
"""Make PUT request.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
endpoint: API endpoint
|
|
247
|
+
data: Request body (will be sent as JSON)
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
Parsed JSON response
|
|
251
|
+
"""
|
|
252
|
+
return self._request_with_retry("PUT", endpoint, json=data)
|
|
253
|
+
|
|
254
|
+
def patch(self, endpoint: str, data: dict | None = None) -> Any:
|
|
255
|
+
"""Make PATCH request.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
endpoint: API endpoint
|
|
259
|
+
data: Request body (will be sent as JSON)
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
Parsed JSON response
|
|
263
|
+
"""
|
|
264
|
+
return self._request_with_retry("PATCH", endpoint, json=data)
|
|
265
|
+
|
|
266
|
+
def delete(self, endpoint: str) -> Any:
|
|
267
|
+
"""Make DELETE request.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
endpoint: API endpoint
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
Parsed JSON response, or None for 204
|
|
274
|
+
"""
|
|
275
|
+
return self._request_with_retry("DELETE", endpoint)
|