tasksmind 0.1.0__tar.gz
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.
- tasksmind-0.1.0/.gitignore +39 -0
- tasksmind-0.1.0/PKG-INFO +124 -0
- tasksmind-0.1.0/README.md +100 -0
- tasksmind-0.1.0/pyproject.toml +36 -0
- tasksmind-0.1.0/tasksmind/__init__.py +96 -0
- tasksmind-0.1.0/tasksmind/_http.py +62 -0
- tasksmind-0.1.0/tasksmind/exceptions.py +35 -0
- tasksmind-0.1.0/tasksmind/resources/__init__.py +3 -0
- tasksmind-0.1.0/tasksmind/resources/runs.py +171 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Logs
|
|
2
|
+
logs
|
|
3
|
+
*.log
|
|
4
|
+
npm-debug.log*
|
|
5
|
+
yarn-debug.log*
|
|
6
|
+
yarn-error.log*
|
|
7
|
+
pnpm-debug.log*
|
|
8
|
+
lerna-debug.log*
|
|
9
|
+
|
|
10
|
+
node_modules
|
|
11
|
+
dist
|
|
12
|
+
dist-ssr
|
|
13
|
+
*.local
|
|
14
|
+
|
|
15
|
+
# Environment variables
|
|
16
|
+
.env
|
|
17
|
+
.env.local
|
|
18
|
+
.env.development.local
|
|
19
|
+
.env.test.local
|
|
20
|
+
.env.production.local
|
|
21
|
+
supabase/.env
|
|
22
|
+
|
|
23
|
+
# Editor directories and files
|
|
24
|
+
.vscode/*
|
|
25
|
+
!.vscode/extensions.json
|
|
26
|
+
.idea
|
|
27
|
+
.DS_Store
|
|
28
|
+
*.suo
|
|
29
|
+
*.ntvs*
|
|
30
|
+
*.njsproj
|
|
31
|
+
*.sln
|
|
32
|
+
*.sw?
|
|
33
|
+
|
|
34
|
+
.env
|
|
35
|
+
.env.local
|
|
36
|
+
# Python
|
|
37
|
+
__pycache__/
|
|
38
|
+
*.pyc
|
|
39
|
+
.modal.toml
|
tasksmind-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tasksmind
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: TasksMind Python SDK — The AI Engineer for Developers on Call.
|
|
5
|
+
Project-URL: Homepage, https://tasksmind.com
|
|
6
|
+
Project-URL: Documentation, https://docs.tasksmind.com
|
|
7
|
+
Project-URL: Repository, https://github.com/tasksmind/tasksmind-python
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/tasksmind/tasksmind-python/issues
|
|
9
|
+
Author-email: TasksMind <support@tasksmind.com>
|
|
10
|
+
License: MIT
|
|
11
|
+
Keywords: ai,automation,devops,oncall,tasksmind
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Requires-Dist: httpx>=0.24.0
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# TasksMind Python SDK
|
|
26
|
+
|
|
27
|
+
The official Python client for the [TasksMind API](https://tasksmind.com) — The AI Engineer for Developers on Call.
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install tasksmind
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Quick Start
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
import os
|
|
39
|
+
from tasksmind import TasksMind
|
|
40
|
+
|
|
41
|
+
client = TasksMind(api_key=os.environ["TASKSMIND_API_KEY"])
|
|
42
|
+
|
|
43
|
+
# Kick off a PR review
|
|
44
|
+
run = client.runs.create(
|
|
45
|
+
repo_url="https://github.com/my-org/my-repo",
|
|
46
|
+
repo_ref="main",
|
|
47
|
+
payload={
|
|
48
|
+
"intent": "review_pr",
|
|
49
|
+
"target": {"pr_number": 42},
|
|
50
|
+
},
|
|
51
|
+
)
|
|
52
|
+
print(f"Run started: {run.id}")
|
|
53
|
+
|
|
54
|
+
# Poll until complete (up to 10 minutes)
|
|
55
|
+
result = client.runs.wait(run.id, timeout_s=600)
|
|
56
|
+
if result.is_success():
|
|
57
|
+
print(result.output)
|
|
58
|
+
else:
|
|
59
|
+
print(f"Run failed with status: {result.status}")
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Configuration
|
|
63
|
+
|
|
64
|
+
| Parameter | Env variable | Default |
|
|
65
|
+
|-----------|-------------|---------|
|
|
66
|
+
| `api_key` | `TASKSMIND_API_KEY` | — (required) |
|
|
67
|
+
| `base_url` | `TASKSMIND_API_BASE_URL` | `https://api.tasksmind.com` |
|
|
68
|
+
| `timeout` | — | `60` seconds |
|
|
69
|
+
|
|
70
|
+
## Available Resources
|
|
71
|
+
|
|
72
|
+
### `client.runs`
|
|
73
|
+
|
|
74
|
+
| Method | Description |
|
|
75
|
+
|--------|-------------|
|
|
76
|
+
| `runs.create(repo_url, repo_ref, payload)` | Create a new run |
|
|
77
|
+
| `runs.get(run_id)` | Fetch a run by ID |
|
|
78
|
+
| `runs.list(limit, offset, status)` | List runs |
|
|
79
|
+
| `runs.wait(run_id, timeout_s, poll_s, terminal_statuses)` | Poll until terminal status |
|
|
80
|
+
|
|
81
|
+
### `Run` object
|
|
82
|
+
|
|
83
|
+
| Attribute | Type | Description |
|
|
84
|
+
|-----------|------|-------------|
|
|
85
|
+
| `.id` | `str` | Run UUID |
|
|
86
|
+
| `.status` | `str` | Current status (`running`, `completed`, `failed`, etc.) |
|
|
87
|
+
| `.output` | `str` | Output text from the run |
|
|
88
|
+
| `.summary` | `str \| None` | Short summary |
|
|
89
|
+
| `.pr_url` | `str \| None` | Pull request URL if one was created |
|
|
90
|
+
| `.pr_number` | `int \| None` | Pull request number |
|
|
91
|
+
| `.is_success()` | `bool` | `True` when status is `completed` or `succeeded` |
|
|
92
|
+
|
|
93
|
+
## Error Handling
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
from tasksmind.exceptions import AuthError, NotFoundError, APIError, TimeoutError
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
result = client.runs.wait(run_id, timeout_s=120)
|
|
100
|
+
except TimeoutError:
|
|
101
|
+
print("Run did not finish in time")
|
|
102
|
+
except AuthError:
|
|
103
|
+
print("Invalid API key")
|
|
104
|
+
except APIError as e:
|
|
105
|
+
print(f"API error {e.status_code}: {e}")
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Context Manager
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
with TasksMind() as client:
|
|
112
|
+
run = client.runs.create(repo_url="https://github.com/org/repo")
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Requirements
|
|
116
|
+
|
|
117
|
+
- Python 3.9+
|
|
118
|
+
- `httpx >= 0.24`
|
|
119
|
+
|
|
120
|
+
## Links
|
|
121
|
+
|
|
122
|
+
- [Website](https://tasksmind.com)
|
|
123
|
+
- [Documentation](https://docs.tasksmind.com)
|
|
124
|
+
- [GitHub](https://github.com/tasksmind/tasksmind-python)
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# TasksMind Python SDK
|
|
2
|
+
|
|
3
|
+
The official Python client for the [TasksMind API](https://tasksmind.com) — The AI Engineer for Developers on Call.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install tasksmind
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
import os
|
|
15
|
+
from tasksmind import TasksMind
|
|
16
|
+
|
|
17
|
+
client = TasksMind(api_key=os.environ["TASKSMIND_API_KEY"])
|
|
18
|
+
|
|
19
|
+
# Kick off a PR review
|
|
20
|
+
run = client.runs.create(
|
|
21
|
+
repo_url="https://github.com/my-org/my-repo",
|
|
22
|
+
repo_ref="main",
|
|
23
|
+
payload={
|
|
24
|
+
"intent": "review_pr",
|
|
25
|
+
"target": {"pr_number": 42},
|
|
26
|
+
},
|
|
27
|
+
)
|
|
28
|
+
print(f"Run started: {run.id}")
|
|
29
|
+
|
|
30
|
+
# Poll until complete (up to 10 minutes)
|
|
31
|
+
result = client.runs.wait(run.id, timeout_s=600)
|
|
32
|
+
if result.is_success():
|
|
33
|
+
print(result.output)
|
|
34
|
+
else:
|
|
35
|
+
print(f"Run failed with status: {result.status}")
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Configuration
|
|
39
|
+
|
|
40
|
+
| Parameter | Env variable | Default |
|
|
41
|
+
|-----------|-------------|---------|
|
|
42
|
+
| `api_key` | `TASKSMIND_API_KEY` | — (required) |
|
|
43
|
+
| `base_url` | `TASKSMIND_API_BASE_URL` | `https://api.tasksmind.com` |
|
|
44
|
+
| `timeout` | — | `60` seconds |
|
|
45
|
+
|
|
46
|
+
## Available Resources
|
|
47
|
+
|
|
48
|
+
### `client.runs`
|
|
49
|
+
|
|
50
|
+
| Method | Description |
|
|
51
|
+
|--------|-------------|
|
|
52
|
+
| `runs.create(repo_url, repo_ref, payload)` | Create a new run |
|
|
53
|
+
| `runs.get(run_id)` | Fetch a run by ID |
|
|
54
|
+
| `runs.list(limit, offset, status)` | List runs |
|
|
55
|
+
| `runs.wait(run_id, timeout_s, poll_s, terminal_statuses)` | Poll until terminal status |
|
|
56
|
+
|
|
57
|
+
### `Run` object
|
|
58
|
+
|
|
59
|
+
| Attribute | Type | Description |
|
|
60
|
+
|-----------|------|-------------|
|
|
61
|
+
| `.id` | `str` | Run UUID |
|
|
62
|
+
| `.status` | `str` | Current status (`running`, `completed`, `failed`, etc.) |
|
|
63
|
+
| `.output` | `str` | Output text from the run |
|
|
64
|
+
| `.summary` | `str \| None` | Short summary |
|
|
65
|
+
| `.pr_url` | `str \| None` | Pull request URL if one was created |
|
|
66
|
+
| `.pr_number` | `int \| None` | Pull request number |
|
|
67
|
+
| `.is_success()` | `bool` | `True` when status is `completed` or `succeeded` |
|
|
68
|
+
|
|
69
|
+
## Error Handling
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from tasksmind.exceptions import AuthError, NotFoundError, APIError, TimeoutError
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
result = client.runs.wait(run_id, timeout_s=120)
|
|
76
|
+
except TimeoutError:
|
|
77
|
+
print("Run did not finish in time")
|
|
78
|
+
except AuthError:
|
|
79
|
+
print("Invalid API key")
|
|
80
|
+
except APIError as e:
|
|
81
|
+
print(f"API error {e.status_code}: {e}")
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Context Manager
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
with TasksMind() as client:
|
|
88
|
+
run = client.runs.create(repo_url="https://github.com/org/repo")
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Requirements
|
|
92
|
+
|
|
93
|
+
- Python 3.9+
|
|
94
|
+
- `httpx >= 0.24`
|
|
95
|
+
|
|
96
|
+
## Links
|
|
97
|
+
|
|
98
|
+
- [Website](https://tasksmind.com)
|
|
99
|
+
- [Documentation](https://docs.tasksmind.com)
|
|
100
|
+
- [GitHub](https://github.com/tasksmind/tasksmind-python)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "tasksmind"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "TasksMind Python SDK — The AI Engineer for Developers on Call."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
authors = [{ name = "TasksMind", email = "support@tasksmind.com" }]
|
|
13
|
+
keywords = ["tasksmind", "ai", "devops", "oncall", "automation"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.9",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
24
|
+
]
|
|
25
|
+
dependencies = [
|
|
26
|
+
"httpx>=0.24.0",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://tasksmind.com"
|
|
31
|
+
Documentation = "https://docs.tasksmind.com"
|
|
32
|
+
Repository = "https://github.com/tasksmind/tasksmind-python"
|
|
33
|
+
"Bug Tracker" = "https://github.com/tasksmind/tasksmind-python/issues"
|
|
34
|
+
|
|
35
|
+
[tool.hatch.build.targets.wheel]
|
|
36
|
+
packages = ["tasksmind"]
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TasksMind Python SDK
|
|
3
|
+
====================
|
|
4
|
+
|
|
5
|
+
The AI Engineer for Developers on Call.
|
|
6
|
+
|
|
7
|
+
Quick start::
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
from tasksmind import TasksMind
|
|
11
|
+
|
|
12
|
+
client = TasksMind(api_key=os.environ["TASKSMIND_API_KEY"])
|
|
13
|
+
|
|
14
|
+
# Create a run
|
|
15
|
+
run = client.runs.create(
|
|
16
|
+
repo_url="https://github.com/my-org/my-repo",
|
|
17
|
+
repo_ref="main",
|
|
18
|
+
payload={"intent": "review_pr", "target": {"pr_number": 42}},
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# Wait for it to finish
|
|
22
|
+
result = client.runs.wait(run.id, timeout_s=600)
|
|
23
|
+
if result.is_success():
|
|
24
|
+
print(result.output)
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import os
|
|
30
|
+
from typing import Optional
|
|
31
|
+
|
|
32
|
+
from ._http import HTTPClient, _DEFAULT_BASE_URL, _DEFAULT_TIMEOUT
|
|
33
|
+
from .exceptions import APIError, AuthError, NotFoundError, RateLimitError, TasksMindError, TimeoutError
|
|
34
|
+
from .resources import Run, RunsResource
|
|
35
|
+
|
|
36
|
+
__version__ = "0.1.0"
|
|
37
|
+
__all__ = [
|
|
38
|
+
"TasksMind",
|
|
39
|
+
"Run",
|
|
40
|
+
# Exceptions
|
|
41
|
+
"TasksMindError",
|
|
42
|
+
"AuthError",
|
|
43
|
+
"NotFoundError",
|
|
44
|
+
"APIError",
|
|
45
|
+
"RateLimitError",
|
|
46
|
+
"TimeoutError",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class TasksMind:
|
|
51
|
+
"""
|
|
52
|
+
TasksMind API client.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
api_key: Your TasksMind API key. Falls back to the ``TASKSMIND_API_KEY``
|
|
56
|
+
environment variable when not provided.
|
|
57
|
+
base_url: Override the API base URL. Defaults to ``https://api.tasksmind.com``.
|
|
58
|
+
Can also be set via the ``TASKSMIND_API_BASE_URL`` environment variable.
|
|
59
|
+
timeout: HTTP request timeout in seconds (default: 60).
|
|
60
|
+
|
|
61
|
+
Raises:
|
|
62
|
+
:exc:`ValueError`: If no API key is provided and ``TASKSMIND_API_KEY`` is not set.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def __init__(
|
|
66
|
+
self,
|
|
67
|
+
api_key: Optional[str] = None,
|
|
68
|
+
*,
|
|
69
|
+
base_url: Optional[str] = None,
|
|
70
|
+
timeout: float = _DEFAULT_TIMEOUT,
|
|
71
|
+
) -> None:
|
|
72
|
+
resolved_key = api_key or os.environ.get("TASKSMIND_API_KEY", "")
|
|
73
|
+
if not resolved_key:
|
|
74
|
+
raise ValueError(
|
|
75
|
+
"No API key provided. Pass api_key= or set the TASKSMIND_API_KEY environment variable."
|
|
76
|
+
)
|
|
77
|
+
resolved_base = (
|
|
78
|
+
base_url
|
|
79
|
+
or os.environ.get("TASKSMIND_API_BASE_URL", "")
|
|
80
|
+
or _DEFAULT_BASE_URL
|
|
81
|
+
)
|
|
82
|
+
self._http = HTTPClient(api_key=resolved_key, base_url=resolved_base, timeout=timeout)
|
|
83
|
+
self.runs = RunsResource(self._http)
|
|
84
|
+
|
|
85
|
+
def close(self) -> None:
|
|
86
|
+
"""Close the underlying HTTP connection pool."""
|
|
87
|
+
self._http.close()
|
|
88
|
+
|
|
89
|
+
def __enter__(self) -> "TasksMind":
|
|
90
|
+
return self
|
|
91
|
+
|
|
92
|
+
def __exit__(self, *args: object) -> None:
|
|
93
|
+
self.close()
|
|
94
|
+
|
|
95
|
+
def __repr__(self) -> str:
|
|
96
|
+
return f"TasksMind(base_url={self._http._base_url!r})"
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Internal HTTP client for the TasksMind SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, Optional
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from .exceptions import APIError, AuthError, NotFoundError, RateLimitError
|
|
10
|
+
|
|
11
|
+
_DEFAULT_BASE_URL = "https://api.tasksmind.com"
|
|
12
|
+
_DEFAULT_TIMEOUT = 60.0
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class HTTPClient:
|
|
16
|
+
"""Thin wrapper around ``httpx.Client`` that handles auth and error mapping."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, api_key: str, base_url: str, timeout: float) -> None:
|
|
19
|
+
self._api_key = api_key
|
|
20
|
+
self._base_url = base_url.rstrip("/")
|
|
21
|
+
self._client = httpx.Client(timeout=timeout)
|
|
22
|
+
|
|
23
|
+
def _headers(self) -> Dict[str, str]:
|
|
24
|
+
return {
|
|
25
|
+
"Authorization": f"Bearer {self._api_key}",
|
|
26
|
+
"Content-Type": "application/json",
|
|
27
|
+
"Accept": "application/json",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
def _raise_for_status(self, resp: httpx.Response) -> None:
|
|
31
|
+
if resp.is_success:
|
|
32
|
+
return
|
|
33
|
+
body = resp.text[:2000]
|
|
34
|
+
code = resp.status_code
|
|
35
|
+
if code in (401, 403):
|
|
36
|
+
raise AuthError(f"Authentication failed (HTTP {code})", status_code=code, body=body)
|
|
37
|
+
if code == 404:
|
|
38
|
+
raise NotFoundError(f"Resource not found (HTTP 404)", status_code=code, body=body)
|
|
39
|
+
if code == 429:
|
|
40
|
+
raise RateLimitError(f"Rate limit exceeded (HTTP 429)", status_code=code, body=body)
|
|
41
|
+
raise APIError(f"API error (HTTP {code}): {body}", status_code=code, body=body)
|
|
42
|
+
|
|
43
|
+
def get(self, path: str, params: Optional[Dict[str, Any]] = None) -> Any:
|
|
44
|
+
url = self._base_url + path
|
|
45
|
+
resp = self._client.get(url, headers=self._headers(), params=params)
|
|
46
|
+
self._raise_for_status(resp)
|
|
47
|
+
return resp.json() if resp.text else {}
|
|
48
|
+
|
|
49
|
+
def post(self, path: str, body: Any = None) -> Any:
|
|
50
|
+
url = self._base_url + path
|
|
51
|
+
resp = self._client.post(url, headers=self._headers(), json=body)
|
|
52
|
+
self._raise_for_status(resp)
|
|
53
|
+
return resp.json() if resp.text else {}
|
|
54
|
+
|
|
55
|
+
def close(self) -> None:
|
|
56
|
+
self._client.close()
|
|
57
|
+
|
|
58
|
+
def __enter__(self) -> "HTTPClient":
|
|
59
|
+
return self
|
|
60
|
+
|
|
61
|
+
def __exit__(self, *args: Any) -> None:
|
|
62
|
+
self.close()
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""TasksMind SDK exceptions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TasksMindError(Exception):
|
|
7
|
+
"""Base exception for all TasksMind SDK errors."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, message: str, status_code: int = 0, body: str = "") -> None:
|
|
10
|
+
super().__init__(message)
|
|
11
|
+
self.status_code = status_code
|
|
12
|
+
self.body = body
|
|
13
|
+
|
|
14
|
+
def __repr__(self) -> str:
|
|
15
|
+
return f"{type(self).__name__}({self!s}, status_code={self.status_code})"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class AuthError(TasksMindError):
|
|
19
|
+
"""Raised when the API key is missing, invalid, or expired (HTTP 401/403)."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class NotFoundError(TasksMindError):
|
|
23
|
+
"""Raised when the requested resource does not exist (HTTP 404)."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class RateLimitError(TasksMindError):
|
|
27
|
+
"""Raised when the API rate limit is exceeded (HTTP 429)."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class APIError(TasksMindError):
|
|
31
|
+
"""Raised for all other API-level errors (HTTP 4xx/5xx)."""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class TimeoutError(TasksMindError):
|
|
35
|
+
"""Raised when a polling call (e.g. runs.wait) exceeds its timeout."""
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""TasksMind runs resource."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from .._http import HTTPClient
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Run:
|
|
13
|
+
"""Represents a TasksMind run."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, data: Dict[str, Any]) -> None:
|
|
16
|
+
self._data = data
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def id(self) -> str:
|
|
20
|
+
return self._data.get("id", "")
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def status(self) -> str:
|
|
24
|
+
return self._data.get("status", "unknown")
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def output(self) -> str:
|
|
28
|
+
return self._data.get("output") or ""
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def pr_url(self) -> Optional[str]:
|
|
32
|
+
return self._data.get("pr_url")
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def pr_number(self) -> Optional[int]:
|
|
36
|
+
return self._data.get("pr_number")
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def summary(self) -> Optional[str]:
|
|
40
|
+
return self._data.get("summary")
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def error(self) -> Optional[str]:
|
|
44
|
+
return self._data.get("error")
|
|
45
|
+
|
|
46
|
+
def is_success(self) -> bool:
|
|
47
|
+
return self.status in ("completed", "succeeded")
|
|
48
|
+
|
|
49
|
+
def __repr__(self) -> str:
|
|
50
|
+
return f"Run(id={self.id!r}, status={self.status!r})"
|
|
51
|
+
|
|
52
|
+
def __getitem__(self, key: str) -> Any:
|
|
53
|
+
return self._data[key]
|
|
54
|
+
|
|
55
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
56
|
+
return self._data.get(key, default)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class RunsResource:
|
|
60
|
+
"""CRUD and polling for TasksMind runs."""
|
|
61
|
+
|
|
62
|
+
def __init__(self, http: "HTTPClient") -> None:
|
|
63
|
+
self._http = http
|
|
64
|
+
|
|
65
|
+
def create(
|
|
66
|
+
self,
|
|
67
|
+
*,
|
|
68
|
+
repo_url: str,
|
|
69
|
+
repo_ref: str = "main",
|
|
70
|
+
payload: Optional[Dict[str, Any]] = None,
|
|
71
|
+
) -> Run:
|
|
72
|
+
"""
|
|
73
|
+
Create a new run.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
repo_url: Full GitHub repository URL (e.g. ``https://github.com/org/repo``).
|
|
77
|
+
repo_ref: Branch, tag, or commit SHA to target (default: ``"main"``).
|
|
78
|
+
payload: Arbitrary key/value context forwarded to the run (intent, target, etc.).
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
A :class:`Run` object with at minimum an ``id`` attribute.
|
|
82
|
+
"""
|
|
83
|
+
body: Dict[str, Any] = {
|
|
84
|
+
"repo_url": repo_url,
|
|
85
|
+
"repo_ref": repo_ref,
|
|
86
|
+
}
|
|
87
|
+
if payload:
|
|
88
|
+
body["payload"] = payload
|
|
89
|
+
data = self._http.post("/v1/runs", body)
|
|
90
|
+
return Run(data)
|
|
91
|
+
|
|
92
|
+
def get(self, run_id: str) -> Run:
|
|
93
|
+
"""
|
|
94
|
+
Fetch an existing run by its ID.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
run_id: The UUID of the run.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
A :class:`Run` object.
|
|
101
|
+
|
|
102
|
+
Raises:
|
|
103
|
+
:exc:`~tasksmind.exceptions.NotFoundError`: If the run does not exist.
|
|
104
|
+
"""
|
|
105
|
+
data = self._http.get(f"/v1/runs/{run_id}")
|
|
106
|
+
return Run(data)
|
|
107
|
+
|
|
108
|
+
def list(
|
|
109
|
+
self,
|
|
110
|
+
*,
|
|
111
|
+
limit: int = 20,
|
|
112
|
+
offset: int = 0,
|
|
113
|
+
status: Optional[str] = None,
|
|
114
|
+
) -> List[Run]:
|
|
115
|
+
"""
|
|
116
|
+
List runs for the authenticated account.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
limit: Maximum number of runs to return (default: 20).
|
|
120
|
+
offset: Pagination offset (default: 0).
|
|
121
|
+
status: Optional status filter (e.g. ``"completed"``, ``"running"``).
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
A list of :class:`Run` objects.
|
|
125
|
+
"""
|
|
126
|
+
params: Dict[str, Any] = {"limit": limit, "offset": offset}
|
|
127
|
+
if status:
|
|
128
|
+
params["status"] = status
|
|
129
|
+
data = self._http.get("/v1/runs", params=params)
|
|
130
|
+
return [Run(r) for r in (data.get("runs") or data if isinstance(data, list) else [])]
|
|
131
|
+
|
|
132
|
+
def wait(
|
|
133
|
+
self,
|
|
134
|
+
run_id: str,
|
|
135
|
+
*,
|
|
136
|
+
timeout_s: float = 600.0,
|
|
137
|
+
poll_s: float = 2.0,
|
|
138
|
+
terminal_statuses: Optional[List[str]] = None,
|
|
139
|
+
) -> Run:
|
|
140
|
+
"""
|
|
141
|
+
Poll a run until it reaches a terminal status or the timeout expires.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
run_id: The UUID of the run to poll.
|
|
145
|
+
timeout_s: Maximum seconds to wait before raising :exc:`~tasksmind.exceptions.TimeoutError`.
|
|
146
|
+
poll_s: Seconds between each poll request (default: 2).
|
|
147
|
+
terminal_statuses: Status strings that stop polling. Defaults to
|
|
148
|
+
``["completed", "succeeded", "failed", "error", "cancelled"]``.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
The final :class:`Run` object.
|
|
152
|
+
|
|
153
|
+
Raises:
|
|
154
|
+
:exc:`~tasksmind.exceptions.TimeoutError`: If the run does not finish within ``timeout_s``.
|
|
155
|
+
"""
|
|
156
|
+
from ..exceptions import TimeoutError as TasksMindTimeoutError
|
|
157
|
+
|
|
158
|
+
if terminal_statuses is None:
|
|
159
|
+
terminal_statuses = ["completed", "succeeded", "failed", "error", "cancelled"]
|
|
160
|
+
|
|
161
|
+
deadline = time.monotonic() + timeout_s
|
|
162
|
+
while True:
|
|
163
|
+
run = self.get(run_id)
|
|
164
|
+
if run.status in terminal_statuses:
|
|
165
|
+
return run
|
|
166
|
+
remaining = deadline - time.monotonic()
|
|
167
|
+
if remaining <= 0:
|
|
168
|
+
raise TasksMindTimeoutError(
|
|
169
|
+
f"Run {run_id!r} did not complete within {timeout_s}s (last status: {run.status!r})"
|
|
170
|
+
)
|
|
171
|
+
time.sleep(min(poll_s, remaining))
|