predev-api 1.0.0__tar.gz → 1.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: predev-api
3
- Version: 1.0.0
3
+ Version: 1.1.0
4
4
  Summary: Python client for the Pre.dev Architect API - Generate comprehensive software specifications
5
5
  Home-page: https://github.com/predotdev/predev-api
6
6
  Author: Pre.dev
@@ -653,3 +653,51 @@ print(f"{result['completed']}/{result['total']} done in {result['totalCreditsUse
653
653
  - Minimum: **0.1 credits per task** ($0.01)
654
654
  - 10x margin on underlying LLM + sandbox compute
655
655
  - 1 credit = $0.10
656
+
657
+ ### Error handling
658
+
659
+ The browser-task endpoints raise typed exceptions for the most common
660
+ gating cases. Every billing-gate exception carries an `action_url` — a
661
+ deep link back to pre.dev that auto-opens the right modal (subscribe /
662
+ buy credits) when the user lands there:
663
+
664
+ ```python
665
+ from predev_api import (
666
+ PredevAPI,
667
+ InsufficientCreditsError,
668
+ SubscriptionRequiredError,
669
+ RateLimitError,
670
+ QueueFullError,
671
+ )
672
+ import webbrowser
673
+
674
+ client = PredevAPI(api_key="your_pre.dev_api_key")
675
+
676
+ try:
677
+ client.browser_agent([{"url": "https://example.com", "instruction": "extract h1"}])
678
+ except InsufficientCreditsError as e:
679
+ # Send the user to the credits modal — `action_url` is the canonical link.
680
+ if e.action_url:
681
+ webbrowser.open(e.action_url)
682
+ except SubscriptionRequiredError as e:
683
+ if e.action_url:
684
+ webbrowser.open(e.action_url)
685
+ except RateLimitError:
686
+ pass # back off and retry
687
+ except QueueFullError:
688
+ pass # wait for in-flight tasks to drain
689
+ ```
690
+
691
+ Mid-stream errors on the SSE stream raise the same typed exceptions, so a
692
+ `for msg in client.browser_agent(..., stream=True):` loop wrapped in
693
+ `try/except` is enough — no special event handling required.
694
+
695
+ | Exception | HTTP | `code` |
696
+ |---|---|---|
697
+ | `SubscriptionRequiredError` | 402 | `SUBSCRIPTION_REQUIRED` |
698
+ | `InsufficientCreditsError` | 402 | `INSUFFICIENT_CREDITS` |
699
+ | `RateLimitError` | 429 | `RATE_LIMITED` |
700
+ | `QueueFullError` | 429 | `QUEUE_FULL` |
701
+ | `BatchTooLargeError` | 400 | `BATCH_TOO_LARGE` |
702
+ | `AuthenticationError` | 401 | — |
703
+ | `PredevAPIError` | other | — |
@@ -613,3 +613,51 @@ print(f"{result['completed']}/{result['total']} done in {result['totalCreditsUse
613
613
  - Minimum: **0.1 credits per task** ($0.01)
614
614
  - 10x margin on underlying LLM + sandbox compute
615
615
  - 1 credit = $0.10
616
+
617
+ ### Error handling
618
+
619
+ The browser-task endpoints raise typed exceptions for the most common
620
+ gating cases. Every billing-gate exception carries an `action_url` — a
621
+ deep link back to pre.dev that auto-opens the right modal (subscribe /
622
+ buy credits) when the user lands there:
623
+
624
+ ```python
625
+ from predev_api import (
626
+ PredevAPI,
627
+ InsufficientCreditsError,
628
+ SubscriptionRequiredError,
629
+ RateLimitError,
630
+ QueueFullError,
631
+ )
632
+ import webbrowser
633
+
634
+ client = PredevAPI(api_key="your_pre.dev_api_key")
635
+
636
+ try:
637
+ client.browser_agent([{"url": "https://example.com", "instruction": "extract h1"}])
638
+ except InsufficientCreditsError as e:
639
+ # Send the user to the credits modal — `action_url` is the canonical link.
640
+ if e.action_url:
641
+ webbrowser.open(e.action_url)
642
+ except SubscriptionRequiredError as e:
643
+ if e.action_url:
644
+ webbrowser.open(e.action_url)
645
+ except RateLimitError:
646
+ pass # back off and retry
647
+ except QueueFullError:
648
+ pass # wait for in-flight tasks to drain
649
+ ```
650
+
651
+ Mid-stream errors on the SSE stream raise the same typed exceptions, so a
652
+ `for msg in client.browser_agent(..., stream=True):` loop wrapped in
653
+ `try/except` is enough — no special event handling required.
654
+
655
+ | Exception | HTTP | `code` |
656
+ |---|---|---|
657
+ | `SubscriptionRequiredError` | 402 | `SUBSCRIPTION_REQUIRED` |
658
+ | `InsufficientCreditsError` | 402 | `INSUFFICIENT_CREDITS` |
659
+ | `RateLimitError` | 429 | `RATE_LIMITED` |
660
+ | `QueueFullError` | 429 | `QUEUE_FULL` |
661
+ | `BatchTooLargeError` | 400 | `BATCH_TOO_LARGE` |
662
+ | `AuthenticationError` | 401 | — |
663
+ | `PredevAPIError` | other | — |
@@ -27,14 +27,28 @@ from .client import (
27
27
  AlternativeTechStackItemApi,
28
28
  SpecEnrichedTechStackItem,
29
29
  )
30
- from .exceptions import PredevAPIError, AuthenticationError, RateLimitError
30
+ from .exceptions import (
31
+ PredevAPIError,
32
+ AuthenticationError,
33
+ RateLimitError,
34
+ SubscriptionRequiredError,
35
+ InsufficientCreditsError,
36
+ QueueFullError,
37
+ BatchTooLargeError,
38
+ exception_for_code,
39
+ )
31
40
 
32
- __version__ = "1.0.0"
41
+ __version__ = "1.1.0"
33
42
  __all__ = [
34
43
  "PredevAPI",
35
44
  "PredevAPIError",
36
45
  "AuthenticationError",
37
46
  "RateLimitError",
47
+ "SubscriptionRequiredError",
48
+ "InsufficientCreditsError",
49
+ "QueueFullError",
50
+ "BatchTooLargeError",
51
+ "exception_for_code",
38
52
  "ZippedDocsUrl",
39
53
  "SpecCoreFunctionality",
40
54
  "SpecTechStackItem",
@@ -6,7 +6,16 @@ from typing import Optional, Dict, Any, Literal, List, Union, BinaryIO, Iterator
6
6
  from dataclasses import dataclass
7
7
  import json
8
8
  import requests
9
- from .exceptions import PredevAPIError, AuthenticationError, RateLimitError
9
+ from .exceptions import (
10
+ PredevAPIError,
11
+ AuthenticationError,
12
+ RateLimitError,
13
+ SubscriptionRequiredError,
14
+ InsufficientCreditsError,
15
+ QueueFullError,
16
+ BatchTooLargeError,
17
+ exception_for_code,
18
+ )
10
19
 
11
20
 
12
21
  @dataclass
@@ -132,7 +141,9 @@ class SpecGraphNode:
132
141
  label: str = ""
133
142
  type: Optional[str] = None
134
143
  description: Optional[str] = None
135
- level: Optional[int] = None
144
+ # userFlowGraph: BFS depth from roles (roles = 0, flows >= 1, None when
145
+ # unreachable). architectureGraph: C4 diagram level ("C1" | "C2").
146
+ level: Optional[Union[int, str]] = None
136
147
  hours: Optional[float] = None
137
148
 
138
149
 
@@ -625,7 +636,7 @@ class PredevAPI:
625
636
  include_events: Include full event timeline (can be large due to screenshots)
626
637
 
627
638
  Returns:
628
- Dict with id, total, completed, results, totalCost, totalCreditsUsed, status
639
+ Dict with id, total, completed, results, totalCreditsUsed, status
629
640
  """
630
641
  qs = "?includeEvents=true" if include_events else ""
631
642
  url = f"{self.base_url}/browser-agent/{batch_id}{qs}"
@@ -710,7 +721,7 @@ class PredevAPI:
710
721
  get_browser_agent(id) for progress.
711
722
 
712
723
  Returns:
713
- Without stream: Dict with id, total, completed, results, totalCost, totalCreditsUsed
724
+ Without stream: Dict with id, total, completed, results, totalCreditsUsed
714
725
  With stream: Iterator yielding dicts with 'event' and 'data' keys:
715
726
  - event='task_event': real-time events (navigation, screenshot, plan, action, etc.)
716
727
  - event='task_result': a single task completed
@@ -782,6 +793,18 @@ class PredevAPI:
782
793
  current_event = line[7:].strip()
783
794
  elif line.startswith("data: ") and current_event:
784
795
  data = json.loads(line[6:])
796
+ # Mid-stream `error` events carry the same
797
+ # `{ error, code, actionUrl? }` shape the REST endpoints
798
+ # do — surface them as typed exceptions so callers can
799
+ # `except InsufficientCreditsError` cleanly.
800
+ if current_event == "error" and isinstance(data, dict):
801
+ raise exception_for_code(
802
+ data.get("code"),
803
+ data.get("error")
804
+ or data.get("message")
805
+ or "Browser agent error",
806
+ data.get("actionUrl") or data.get("action_url"),
807
+ )
785
808
  yield {"event": current_event, "data": data}
786
809
  current_event = ""
787
810
  except requests.RequestException as e:
@@ -946,22 +969,45 @@ class PredevAPI:
946
969
  return {"file": (filename, file)}
947
970
 
948
971
  def _handle_response(self, response: requests.Response) -> None:
949
- """Handle API response and raise appropriate exceptions."""
972
+ """Handle API response and raise appropriate exceptions.
973
+
974
+ Browser-task gating errors come back as a structured body
975
+ ``{"error": ..., "code": ..., "actionUrl": ...}`` (HTTP 402 / 429 /
976
+ 400). We parse the body before short-circuiting on status code so
977
+ ``code: SUBSCRIPTION_REQUIRED`` becomes :class:`SubscriptionRequiredError`
978
+ with its ``action_url`` populated, instead of a generic
979
+ :class:`PredevAPIError`.
980
+ """
950
981
  if response.status_code == 200:
951
982
  return
952
983
 
984
+ # Try to parse the structured error body up front.
985
+ error_body = None
986
+ error_message = "Unknown error"
987
+ try:
988
+ error_body = response.json()
989
+ error_message = (
990
+ error_body.get("error")
991
+ or error_body.get("message")
992
+ or str(error_body)
993
+ )
994
+ except Exception:
995
+ error_message = response.text or f"HTTP {response.status_code}"
996
+
997
+ # Structured `code` wins — covers SUBSCRIPTION_REQUIRED /
998
+ # INSUFFICIENT_CREDITS / QUEUE_FULL / BATCH_TOO_LARGE / RATE_LIMITED.
999
+ if isinstance(error_body, dict) and error_body.get("code"):
1000
+ raise exception_for_code(
1001
+ error_body["code"],
1002
+ error_message,
1003
+ error_body.get("actionUrl") or error_body.get("action_url"),
1004
+ )
1005
+
953
1006
  if response.status_code == 401:
954
1007
  raise AuthenticationError("Invalid API key")
955
1008
 
956
1009
  if response.status_code == 429:
957
- raise RateLimitError("Rate limit exceeded")
958
-
959
- try:
960
- error_data = response.json()
961
- error_message = error_data.get("error") or error_data.get(
962
- "message") or str(error_data)
963
- except Exception:
964
- error_message = response.text or "Unknown error"
1010
+ raise RateLimitError(error_message)
965
1011
 
966
1012
  raise PredevAPIError(
967
1013
  f"API request failed with status {response.status_code}: {error_message}"
@@ -0,0 +1,86 @@
1
+ """
2
+ Custom exceptions for the Pre.dev API client.
3
+
4
+ Browser-task gating errors carry the same shape as the Node SDK:
5
+ ``code`` selects a typed exception, and ``action_url`` is a deep link
6
+ back to pre.dev that auto-opens the right billing modal (subscribe /
7
+ buy credits) when the user lands there. Older endpoints just return
8
+ ``{"error": "..."}`` and surface as ``PredevAPIError``.
9
+ """
10
+
11
+ from typing import Optional
12
+
13
+
14
+ class PredevAPIError(Exception):
15
+ """Base exception for Pre.dev API errors."""
16
+ pass
17
+
18
+
19
+ class AuthenticationError(PredevAPIError):
20
+ """Raised when authentication fails (HTTP 401)."""
21
+ pass
22
+
23
+
24
+ class RateLimitError(PredevAPIError):
25
+ """Raised when a request is rate-limited (HTTP 429, ``code: RATE_LIMITED``)."""
26
+ pass
27
+
28
+
29
+ class SubscriptionRequiredError(PredevAPIError):
30
+ """HTTP 402, ``code: SUBSCRIPTION_REQUIRED``.
31
+
32
+ Trial limit reached on this API key — the user needs an active
33
+ subscription to keep running browser-agent tasks.
34
+
35
+ ``action_url``, when present, is a deep link back to pre.dev that
36
+ auto-opens the subscribe modal.
37
+ """
38
+
39
+ def __init__(self, message: str, action_url: Optional[str] = None):
40
+ super().__init__(message)
41
+ self.action_url = action_url
42
+
43
+
44
+ class InsufficientCreditsError(PredevAPIError):
45
+ """HTTP 402, ``code: INSUFFICIENT_CREDITS``.
46
+
47
+ Subscription is fine but the credit balance is too low to cover this
48
+ request. ``action_url`` deep-links to the credit-purchase modal.
49
+ """
50
+
51
+ def __init__(self, message: str, action_url: Optional[str] = None):
52
+ super().__init__(message)
53
+ self.action_url = action_url
54
+
55
+
56
+ class QueueFullError(PredevAPIError):
57
+ """HTTP 429, ``code: QUEUE_FULL`` — too many in-flight tasks for this user."""
58
+ pass
59
+
60
+
61
+ class BatchTooLargeError(PredevAPIError):
62
+ """HTTP 400, ``code: BATCH_TOO_LARGE`` — request had more tasks than the per-batch maximum."""
63
+ pass
64
+
65
+
66
+ def exception_for_code(
67
+ code: Optional[str],
68
+ message: str,
69
+ action_url: Optional[str] = None,
70
+ ) -> PredevAPIError:
71
+ """Map a structured error ``code`` to the right typed exception.
72
+
73
+ Falls back to :class:`PredevAPIError` for unknown codes so callers
74
+ that just want ``str(e)`` keep working.
75
+ """
76
+ if code == "SUBSCRIPTION_REQUIRED":
77
+ return SubscriptionRequiredError(message, action_url)
78
+ if code == "INSUFFICIENT_CREDITS":
79
+ return InsufficientCreditsError(message, action_url)
80
+ if code == "QUEUE_FULL":
81
+ return QueueFullError(message)
82
+ if code == "BATCH_TOO_LARGE":
83
+ return BatchTooLargeError(message)
84
+ if code == "RATE_LIMITED":
85
+ return RateLimitError(message)
86
+ return PredevAPIError(message)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: predev-api
3
- Version: 1.0.0
3
+ Version: 1.1.0
4
4
  Summary: Python client for the Pre.dev Architect API - Generate comprehensive software specifications
5
5
  Home-page: https://github.com/predotdev/predev-api
6
6
  Author: Pre.dev
@@ -653,3 +653,51 @@ print(f"{result['completed']}/{result['total']} done in {result['totalCreditsUse
653
653
  - Minimum: **0.1 credits per task** ($0.01)
654
654
  - 10x margin on underlying LLM + sandbox compute
655
655
  - 1 credit = $0.10
656
+
657
+ ### Error handling
658
+
659
+ The browser-task endpoints raise typed exceptions for the most common
660
+ gating cases. Every billing-gate exception carries an `action_url` — a
661
+ deep link back to pre.dev that auto-opens the right modal (subscribe /
662
+ buy credits) when the user lands there:
663
+
664
+ ```python
665
+ from predev_api import (
666
+ PredevAPI,
667
+ InsufficientCreditsError,
668
+ SubscriptionRequiredError,
669
+ RateLimitError,
670
+ QueueFullError,
671
+ )
672
+ import webbrowser
673
+
674
+ client = PredevAPI(api_key="your_pre.dev_api_key")
675
+
676
+ try:
677
+ client.browser_agent([{"url": "https://example.com", "instruction": "extract h1"}])
678
+ except InsufficientCreditsError as e:
679
+ # Send the user to the credits modal — `action_url` is the canonical link.
680
+ if e.action_url:
681
+ webbrowser.open(e.action_url)
682
+ except SubscriptionRequiredError as e:
683
+ if e.action_url:
684
+ webbrowser.open(e.action_url)
685
+ except RateLimitError:
686
+ pass # back off and retry
687
+ except QueueFullError:
688
+ pass # wait for in-flight tasks to drain
689
+ ```
690
+
691
+ Mid-stream errors on the SSE stream raise the same typed exceptions, so a
692
+ `for msg in client.browser_agent(..., stream=True):` loop wrapped in
693
+ `try/except` is enough — no special event handling required.
694
+
695
+ | Exception | HTTP | `code` |
696
+ |---|---|---|
697
+ | `SubscriptionRequiredError` | 402 | `SUBSCRIPTION_REQUIRED` |
698
+ | `InsufficientCreditsError` | 402 | `INSUFFICIENT_CREDITS` |
699
+ | `RateLimitError` | 429 | `RATE_LIMITED` |
700
+ | `QueueFullError` | 429 | `QUEUE_FULL` |
701
+ | `BatchTooLargeError` | 400 | `BATCH_TOO_LARGE` |
702
+ | `AuthenticationError` | 401 | — |
703
+ | `PredevAPIError` | other | — |
@@ -9,7 +9,7 @@ with open("README.md", "r", encoding="utf-8") as fh:
9
9
 
10
10
  setup(
11
11
  name="predev-api",
12
- version="1.0.0",
12
+ version="1.1.0",
13
13
  author="Pre.dev",
14
14
  author_email="support@pre.dev",
15
15
  description="Python client for the Pre.dev Architect API - Generate comprehensive software specifications",
@@ -1,18 +0,0 @@
1
- """
2
- Custom exceptions for the Pre.dev API client
3
- """
4
-
5
-
6
- class PredevAPIError(Exception):
7
- """Base exception for Pre.dev API errors."""
8
- pass
9
-
10
-
11
- class AuthenticationError(PredevAPIError):
12
- """Raised when authentication fails."""
13
- pass
14
-
15
-
16
- class RateLimitError(PredevAPIError):
17
- """Raised when rate limit is exceeded."""
18
- pass
File without changes
File without changes
File without changes