boreholeai 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.
@@ -0,0 +1,7 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .venv/
7
+ .env
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 BoreholeAI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,119 @@
1
+ Metadata-Version: 2.4
2
+ Name: boreholeai
3
+ Version: 0.1.0
4
+ Summary: Python SDK for the BoreholeAI API — digitise borehole logs programmatically
5
+ Project-URL: Homepage, https://boreholeai.com
6
+ Project-URL: Documentation, https://boreholeai.com/docs
7
+ Project-URL: Repository, https://github.com/boreholeai/boreholeai-python
8
+ Author-email: BoreholeAI <support@boreholeai.com>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: api,borehole,boreholeai,digitisation,geotechnical
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Topic :: Scientific/Engineering
18
+ Requires-Python: >=3.9
19
+ Requires-Dist: httpx<1,>=0.24
20
+ Description-Content-Type: text/markdown
21
+
22
+ # BoreholeAI Python SDK
23
+
24
+ Python SDK for the [BoreholeAI](https://boreholeai.com) API. Digitise borehole logs programmatically — upload PDFs or images, get structured ground profiles, test data, and annotated PDFs.
25
+
26
+ ## Installation
27
+
28
+ ```bash
29
+ pip install boreholeai
30
+ ```
31
+
32
+ ## Quick Start
33
+
34
+ ```python
35
+ from boreholeai import BoreholeAI
36
+
37
+ client = BoreholeAI(api_key="bhai_your_api_key_here")
38
+
39
+ # Process a single borehole log
40
+ result = client.process_documents("BH01.pdf", output_dir="./results")
41
+
42
+ print(f"Pages processed: {result.num_pages}")
43
+ for f in result.files:
44
+ print(f" {f.filename}")
45
+ ```
46
+
47
+ ## Folder Processing
48
+
49
+ Pass a directory path to process multiple files together. Results are merged into a single ground profile Excel, test data Excel, and AGS file, with one annotated PDF per input file.
50
+
51
+ ```python
52
+ result = client.process_documents("./borehole_logs/", output_dir="./results")
53
+
54
+ # Output:
55
+ # Borehole_ground_profile.xlsx (merged from all input files)
56
+ # Borehole_test_data.xlsx (merged from all input files)
57
+ # Borehole_ags4.ags (merged from all input files)
58
+ # BH01_annotated.pdf (one per input file)
59
+ # BH02_annotated.pdf
60
+ # BH03_annotated.pdf
61
+ ```
62
+
63
+ ## Supported File Types
64
+
65
+ - PDF (`.pdf`)
66
+ - PNG (`.png`)
67
+ - JPEG (`.jpg`, `.jpeg`)
68
+ - TIFF (`.tif`, `.tiff`)
69
+ - WebP (`.webp`)
70
+
71
+ ## API Key
72
+
73
+ Get your API key from the [BoreholeAI dashboard](https://boreholeai.com/app/settings/api-keys).
74
+
75
+ ```python
76
+ # Pass directly
77
+ client = BoreholeAI(api_key="bhai_xxx")
78
+
79
+ # Or use for local development / testing
80
+ client = BoreholeAI(api_key="bhai_xxx", base_url="http://localhost:8000")
81
+ ```
82
+
83
+ ## Error Handling
84
+
85
+ ```python
86
+ from boreholeai import BoreholeAI, InsufficientCreditsError, AuthenticationError
87
+
88
+ client = BoreholeAI(api_key="bhai_xxx")
89
+
90
+ try:
91
+ result = client.process_documents("borehole.pdf")
92
+ except AuthenticationError:
93
+ print("Invalid API key")
94
+ except InsufficientCreditsError:
95
+ print("Not enough credits — buy more at boreholeai.com")
96
+ ```
97
+
98
+ ## Response
99
+
100
+ `process_documents()` returns a `JobResult`:
101
+
102
+ ```python
103
+ @dataclass
104
+ class JobResult:
105
+ job_id: str # Unique job identifier
106
+ status: str # "completed"
107
+ num_pages: int # Total pages processed
108
+ credits_used: int # Credits consumed
109
+ files: list[FileResult] # Downloaded result files
110
+
111
+ @dataclass
112
+ class FileResult:
113
+ filename: str # e.g. "Borehole_ground_profile.xlsx"
114
+ path: Path # Local path where file was saved
115
+ ```
116
+
117
+ ## License
118
+
119
+ MIT
@@ -0,0 +1,98 @@
1
+ # BoreholeAI Python SDK
2
+
3
+ Python SDK for the [BoreholeAI](https://boreholeai.com) API. Digitise borehole logs programmatically — upload PDFs or images, get structured ground profiles, test data, and annotated PDFs.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install boreholeai
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```python
14
+ from boreholeai import BoreholeAI
15
+
16
+ client = BoreholeAI(api_key="bhai_your_api_key_here")
17
+
18
+ # Process a single borehole log
19
+ result = client.process_documents("BH01.pdf", output_dir="./results")
20
+
21
+ print(f"Pages processed: {result.num_pages}")
22
+ for f in result.files:
23
+ print(f" {f.filename}")
24
+ ```
25
+
26
+ ## Folder Processing
27
+
28
+ Pass a directory path to process multiple files together. Results are merged into a single ground profile Excel, test data Excel, and AGS file, with one annotated PDF per input file.
29
+
30
+ ```python
31
+ result = client.process_documents("./borehole_logs/", output_dir="./results")
32
+
33
+ # Output:
34
+ # Borehole_ground_profile.xlsx (merged from all input files)
35
+ # Borehole_test_data.xlsx (merged from all input files)
36
+ # Borehole_ags4.ags (merged from all input files)
37
+ # BH01_annotated.pdf (one per input file)
38
+ # BH02_annotated.pdf
39
+ # BH03_annotated.pdf
40
+ ```
41
+
42
+ ## Supported File Types
43
+
44
+ - PDF (`.pdf`)
45
+ - PNG (`.png`)
46
+ - JPEG (`.jpg`, `.jpeg`)
47
+ - TIFF (`.tif`, `.tiff`)
48
+ - WebP (`.webp`)
49
+
50
+ ## API Key
51
+
52
+ Get your API key from the [BoreholeAI dashboard](https://boreholeai.com/app/settings/api-keys).
53
+
54
+ ```python
55
+ # Pass directly
56
+ client = BoreholeAI(api_key="bhai_xxx")
57
+
58
+ # Or use for local development / testing
59
+ client = BoreholeAI(api_key="bhai_xxx", base_url="http://localhost:8000")
60
+ ```
61
+
62
+ ## Error Handling
63
+
64
+ ```python
65
+ from boreholeai import BoreholeAI, InsufficientCreditsError, AuthenticationError
66
+
67
+ client = BoreholeAI(api_key="bhai_xxx")
68
+
69
+ try:
70
+ result = client.process_documents("borehole.pdf")
71
+ except AuthenticationError:
72
+ print("Invalid API key")
73
+ except InsufficientCreditsError:
74
+ print("Not enough credits — buy more at boreholeai.com")
75
+ ```
76
+
77
+ ## Response
78
+
79
+ `process_documents()` returns a `JobResult`:
80
+
81
+ ```python
82
+ @dataclass
83
+ class JobResult:
84
+ job_id: str # Unique job identifier
85
+ status: str # "completed"
86
+ num_pages: int # Total pages processed
87
+ credits_used: int # Credits consumed
88
+ files: list[FileResult] # Downloaded result files
89
+
90
+ @dataclass
91
+ class FileResult:
92
+ filename: str # e.g. "Borehole_ground_profile.xlsx"
93
+ path: Path # Local path where file was saved
94
+ ```
95
+
96
+ ## License
97
+
98
+ MIT
@@ -0,0 +1,25 @@
1
+ """Basic usage example for the BoreholeAI Python SDK."""
2
+
3
+ from boreholeai import BoreholeAI
4
+
5
+ # Initialize client with your API key
6
+ client = BoreholeAI(api_key="bhai_your_api_key_here")
7
+
8
+ # Process a single PDF
9
+ result = client.process_documents("path/to/borehole.pdf", output_dir="./results")
10
+
11
+ print(f"Job ID: {result.job_id}")
12
+ print(f"Pages processed: {result.num_pages}")
13
+ print(f"Credits used: {result.credits_used}")
14
+ print(f"Output files:")
15
+ for f in result.files:
16
+ print(f" {f.filename} → {f.path}")
17
+
18
+
19
+ # Process a folder of PDFs and images (merged output)
20
+ result = client.process_documents("path/to/logs/", output_dir="./results")
21
+
22
+ print(f"\nBatch job ID: {result.job_id}")
23
+ print(f"Total pages: {result.num_pages}")
24
+ for f in result.files:
25
+ print(f" {f.filename} → {f.path}")
@@ -0,0 +1,32 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "boreholeai"
7
+ version = "0.1.0"
8
+ description = "Python SDK for the BoreholeAI API — digitise borehole logs programmatically"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.9"
12
+ authors = [{ name = "BoreholeAI", email = "support@boreholeai.com" }]
13
+ keywords = ["boreholeai", "geotechnical", "borehole", "digitisation", "api"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "Intended Audience :: Science/Research",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Programming Language :: Python :: 3",
20
+ "Topic :: Scientific/Engineering",
21
+ ]
22
+ dependencies = [
23
+ "httpx>=0.24,<1",
24
+ ]
25
+
26
+ [project.urls]
27
+ Homepage = "https://boreholeai.com"
28
+ Documentation = "https://boreholeai.com/docs"
29
+ Repository = "https://github.com/boreholeai/boreholeai-python"
30
+
31
+ [tool.hatch.build.targets.wheel]
32
+ packages = ["src/boreholeai"]
@@ -0,0 +1,26 @@
1
+ """BoreholeAI Python SDK — digitise borehole logs programmatically."""
2
+
3
+ from boreholeai._types import FileResult, JobResult
4
+ from boreholeai._version import __version__
5
+ from boreholeai.client import BoreholeAI
6
+ from boreholeai.exceptions import (
7
+ AuthenticationError,
8
+ BoreholeAIError,
9
+ InsufficientCreditsError,
10
+ JobFailedError,
11
+ RateLimitError,
12
+ ServerError,
13
+ )
14
+
15
+ __all__ = [
16
+ "BoreholeAI",
17
+ "__version__",
18
+ "AuthenticationError",
19
+ "BoreholeAIError",
20
+ "FileResult",
21
+ "InsufficientCreditsError",
22
+ "JobFailedError",
23
+ "JobResult",
24
+ "RateLimitError",
25
+ "ServerError",
26
+ ]
@@ -0,0 +1,86 @@
1
+ """Low-level HTTP calls to the BoreholeAI worker API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ import httpx
9
+
10
+ from boreholeai.exceptions import (
11
+ AuthenticationError,
12
+ BoreholeAIError,
13
+ InsufficientCreditsError,
14
+ RateLimitError,
15
+ ServerError,
16
+ )
17
+
18
+ DEFAULT_BASE_URL = "https://boreholeai-worker-7b5nhwzhwq-uc.a.run.app"
19
+ DEFAULT_TIMEOUT = 120.0
20
+
21
+
22
+ class APIClient:
23
+ """Thin wrapper around httpx for authenticated requests to the worker."""
24
+
25
+ def __init__(self, api_key: str, base_url: str, timeout: float):
26
+ self._client = httpx.Client(
27
+ base_url=base_url,
28
+ headers={"Authorization": f"Bearer {api_key}"},
29
+ timeout=timeout,
30
+ )
31
+
32
+ def close(self) -> None:
33
+ self._client.close()
34
+
35
+ def create_job(self, file_paths: list[Path]) -> dict[str, Any]:
36
+ """POST /v1/jobs — upload files and create a job.
37
+
38
+ Returns {"job_id": ..., "num_pages": ..., "credits_remaining": ...}.
39
+ """
40
+ files = [
41
+ ("files", (p.name, p.read_bytes()))
42
+ for p in file_paths
43
+ ]
44
+ resp = self._client.post("/v1/jobs", files=files)
45
+ _raise_for_status(resp)
46
+ return resp.json()
47
+
48
+ def get_job(self, job_id: str) -> dict[str, Any]:
49
+ """GET /v1/jobs/{id} — poll job status."""
50
+ resp = self._client.get(f"/v1/jobs/{job_id}")
51
+ _raise_for_status(resp)
52
+ return resp.json()
53
+
54
+ def get_results(self, job_id: str) -> dict[str, Any]:
55
+ """GET /v1/jobs/{id}/results — signed download URLs."""
56
+ resp = self._client.get(f"/v1/jobs/{job_id}/results")
57
+ _raise_for_status(resp)
58
+ return resp.json()
59
+
60
+ def download_file(self, url: str) -> bytes:
61
+ """Download a file from a signed URL."""
62
+ resp = httpx.get(url, timeout=self._client.timeout)
63
+ resp.raise_for_status()
64
+ return resp.content
65
+
66
+
67
+ def _raise_for_status(resp: httpx.Response) -> None:
68
+ """Convert HTTP errors to SDK exceptions."""
69
+ if resp.is_success:
70
+ return
71
+
72
+ detail = ""
73
+ try:
74
+ detail = resp.json().get("detail", "")
75
+ except Exception:
76
+ detail = resp.text
77
+
78
+ if resp.status_code == 401:
79
+ raise AuthenticationError(detail or "Invalid API key", 401)
80
+ if resp.status_code == 402:
81
+ raise InsufficientCreditsError(detail or "Insufficient credits", 402)
82
+ if resp.status_code == 429:
83
+ raise RateLimitError(detail or "Rate limited", 429)
84
+ if resp.status_code >= 500:
85
+ raise ServerError(detail or "Server error", resp.status_code)
86
+ raise BoreholeAIError(detail or f"HTTP {resp.status_code}", resp.status_code)
@@ -0,0 +1,47 @@
1
+ """File scanning and validation utilities."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ SUPPORTED_EXTENSIONS = {".pdf", ".png", ".jpg", ".jpeg", ".tif", ".tiff", ".webp"}
8
+
9
+
10
+ def collect_files(input_path: str | Path) -> list[Path]:
11
+ """Collect valid files from a path (single file or directory).
12
+
13
+ Returns a sorted list of Paths with supported extensions.
14
+ Raises FileNotFoundError if the path doesn't exist.
15
+ Raises ValueError if no supported files are found.
16
+ """
17
+ path = Path(input_path).resolve()
18
+
19
+ if not path.exists():
20
+ raise FileNotFoundError(f"Path not found: {path}")
21
+
22
+ if path.is_file():
23
+ _validate_extension(path)
24
+ return [path]
25
+
26
+ if path.is_dir():
27
+ files = sorted(
28
+ f for f in path.iterdir()
29
+ if f.is_file() and f.suffix.lower() in SUPPORTED_EXTENSIONS
30
+ )
31
+ if not files:
32
+ raise ValueError(
33
+ f"No supported files found in {path}. "
34
+ f"Accepted: {', '.join(sorted(SUPPORTED_EXTENSIONS))}"
35
+ )
36
+ return files
37
+
38
+ raise ValueError(f"Path is neither a file nor a directory: {path}")
39
+
40
+
41
+ def _validate_extension(path: Path) -> None:
42
+ """Raise ValueError if the file extension is not supported."""
43
+ if path.suffix.lower() not in SUPPORTED_EXTENSIONS:
44
+ raise ValueError(
45
+ f"Unsupported file type: {path.suffix}. "
46
+ f"Accepted: {', '.join(sorted(SUPPORTED_EXTENSIONS))}"
47
+ )
@@ -0,0 +1,25 @@
1
+ """Response types for the BoreholeAI SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from pathlib import Path
7
+
8
+
9
+ @dataclass
10
+ class FileResult:
11
+ """A single downloaded result file."""
12
+
13
+ filename: str
14
+ path: Path
15
+
16
+
17
+ @dataclass
18
+ class JobResult:
19
+ """Result of processing a single job (one or more input files)."""
20
+
21
+ job_id: str
22
+ status: str
23
+ num_pages: int
24
+ credits_used: int
25
+ files: list[FileResult] = field(default_factory=list)
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,164 @@
1
+ """BoreholeAI Python SDK client."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ import time
7
+ from pathlib import Path
8
+
9
+ from boreholeai._api import APIClient, DEFAULT_BASE_URL, DEFAULT_TIMEOUT
10
+ from boreholeai._files import collect_files
11
+ from boreholeai._types import FileResult, JobResult
12
+ from boreholeai.exceptions import JobFailedError
13
+
14
+ # Polling configuration
15
+ _POLL_INITIAL_INTERVAL = 2.0 # seconds
16
+ _POLL_MAX_INTERVAL = 10.0 # seconds
17
+ _POLL_BACKOFF_FACTOR = 1.5
18
+
19
+ # Default output directory
20
+ _DEFAULT_OUTPUT_DIR = "./boreholeai_output"
21
+
22
+
23
+ class BoreholeAI:
24
+ """Client for the BoreholeAI API.
25
+
26
+ Usage::
27
+
28
+ from boreholeai import BoreholeAI
29
+
30
+ client = BoreholeAI(api_key="bhai_xxx")
31
+
32
+ # Single file
33
+ results = client.process_documents("borehole.pdf")
34
+
35
+ # Folder — all PDFs + images processed together, merged output
36
+ results = client.process_documents("./logs/", output_dir="./results")
37
+ """
38
+
39
+ def __init__(
40
+ self,
41
+ api_key: str,
42
+ *,
43
+ base_url: str = DEFAULT_BASE_URL,
44
+ timeout: float = DEFAULT_TIMEOUT,
45
+ ):
46
+ if not api_key:
47
+ raise ValueError("api_key is required")
48
+ self._api = APIClient(api_key=api_key, base_url=base_url, timeout=timeout)
49
+
50
+ def close(self) -> None:
51
+ """Close the underlying HTTP client."""
52
+ self._api.close()
53
+
54
+ def __enter__(self) -> BoreholeAI:
55
+ return self
56
+
57
+ def __exit__(self, *args: object) -> None:
58
+ self.close()
59
+
60
+ def process_documents(
61
+ self,
62
+ input_path: str | Path,
63
+ *,
64
+ output_dir: str | Path = _DEFAULT_OUTPUT_DIR,
65
+ ) -> JobResult:
66
+ """Upload files, process them, and download results.
67
+
68
+ Args:
69
+ input_path: Path to a single file or a directory of files.
70
+ Supported formats: PDF, PNG, JPG, JPEG, TIF, TIFF, WebP.
71
+ output_dir: Directory to save result files. Created if it doesn't exist.
72
+
73
+ Returns:
74
+ JobResult with job metadata and list of downloaded files.
75
+
76
+ Raises:
77
+ FileNotFoundError: If input_path doesn't exist.
78
+ ValueError: If no supported files found.
79
+ AuthenticationError: If API key is invalid.
80
+ InsufficientCreditsError: If not enough credits.
81
+ JobFailedError: If processing fails on the server.
82
+ """
83
+ files = collect_files(input_path)
84
+ out = Path(output_dir).resolve()
85
+ out.mkdir(parents=True, exist_ok=True)
86
+
87
+ # Upload
88
+ total_files = len(files)
89
+ _print_status(f"Uploading {total_files} file(s)...")
90
+ job_data = self._api.create_job(files)
91
+ job_id = job_data["job_id"]
92
+ num_pages = job_data["num_pages"]
93
+ _print_status(
94
+ f"Job {job_id[:8]}... created — "
95
+ f"{num_pages} page(s), {total_files} file(s)"
96
+ )
97
+
98
+ # Poll
99
+ result = self._poll_until_done(job_id)
100
+
101
+ # Download results
102
+ downloaded = self._download_results(job_id, out)
103
+
104
+ return JobResult(
105
+ job_id=job_id,
106
+ status=result["status"],
107
+ num_pages=num_pages,
108
+ credits_used=num_pages,
109
+ files=downloaded,
110
+ )
111
+
112
+ def _poll_until_done(self, job_id: str) -> dict:
113
+ """Poll GET /v1/jobs/{id} until status is completed or failed."""
114
+ interval = _POLL_INITIAL_INTERVAL
115
+
116
+ while True:
117
+ data = self._api.get_job(job_id)
118
+ status = data["status"]
119
+
120
+ if status == "completed":
121
+ _print_status("Processing complete!")
122
+ return data
123
+
124
+ if status == "failed":
125
+ error = data.get("error_message", "Unknown error")
126
+ raise JobFailedError(f"Job {job_id} failed: {error}")
127
+
128
+ # Show progress if available
129
+ progress = data.get("progress") or {}
130
+ pages_done = progress.get("pages_done", 0)
131
+ pages_total = progress.get("pages_total", "?")
132
+ _print_status(
133
+ f"Processing... {pages_done}/{pages_total} pages "
134
+ f"[{status}]"
135
+ )
136
+
137
+ time.sleep(interval)
138
+ interval = min(interval * _POLL_BACKOFF_FACTOR, _POLL_MAX_INTERVAL)
139
+
140
+ def _download_results(self, job_id: str, output_dir: Path) -> list[FileResult]:
141
+ """Download all result files to output_dir."""
142
+ results_data = self._api.get_results(job_id)
143
+ downloaded: list[FileResult] = []
144
+
145
+ for file_info in results_data.get("files", []):
146
+ filename = file_info["filename"]
147
+ url = file_info["url"]
148
+
149
+ _print_status(f"Downloading {filename}...")
150
+ content = self._api.download_file(url)
151
+
152
+ dest = output_dir / filename
153
+ dest.write_bytes(content)
154
+ downloaded.append(FileResult(filename=filename, path=dest))
155
+
156
+ _print_status(
157
+ f"Downloaded {len(downloaded)} file(s) to {output_dir}"
158
+ )
159
+ return downloaded
160
+
161
+
162
+ def _print_status(message: str) -> None:
163
+ """Print a status message to stderr (doesn't interfere with stdout piping)."""
164
+ print(f" {message}", file=sys.stderr, flush=True)
@@ -0,0 +1,29 @@
1
+ """BoreholeAI SDK exceptions."""
2
+
3
+
4
+ class BoreholeAIError(Exception):
5
+ """Base exception for all SDK errors."""
6
+
7
+ def __init__(self, message: str, status_code: int | None = None):
8
+ super().__init__(message)
9
+ self.status_code = status_code
10
+
11
+
12
+ class AuthenticationError(BoreholeAIError):
13
+ """Invalid or missing API key (401)."""
14
+
15
+
16
+ class InsufficientCreditsError(BoreholeAIError):
17
+ """Not enough credits to process the request (402)."""
18
+
19
+
20
+ class RateLimitError(BoreholeAIError):
21
+ """Too many requests (429)."""
22
+
23
+
24
+ class ServerError(BoreholeAIError):
25
+ """Worker returned 5xx."""
26
+
27
+
28
+ class JobFailedError(BoreholeAIError):
29
+ """Job processing failed on the server."""