secondopinion-sdk 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,12 @@
1
+ node_modules
2
+ dist
3
+ .env
4
+ .env.local
5
+ .env*.local
6
+ .next
7
+ .vercel
8
+ *.tsbuildinfo
9
+ *.tgz
10
+ nul
11
+ .2ndopinion/
12
+ docs/specs/
@@ -0,0 +1,64 @@
1
+ Metadata-Version: 2.4
2
+ Name: secondopinion-sdk
3
+ Version: 0.1.0
4
+ Summary: Python SDK for the 2ndOpinion API
5
+ Project-URL: Homepage, https://get2ndopinion.dev
6
+ Project-URL: Repository, https://github.com/brianmello8/2ndopinion
7
+ Author-email: 2ndOpinion <hello@get2ndopinion.dev>
8
+ License: MIT
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Programming Language :: Python :: 3
12
+ Requires-Python: >=3.8
13
+ Requires-Dist: httpx>=0.24.0
14
+ Description-Content-Type: text/markdown
15
+
16
+ # secondopinion
17
+
18
+ Python SDK for the [2ndOpinion](https://get2ndopinion.dev) API — AI-to-AI code review.
19
+
20
+ ## Installation
21
+
22
+ ```bash
23
+ pip install secondopinion-sdk
24
+ ```
25
+
26
+ ## Quick Start
27
+
28
+ ```python
29
+ from secondopinion import SecondOpinion
30
+
31
+ client = SecondOpinion(api_key="sk_2op_...")
32
+
33
+ # Get a code review
34
+ result = client.opinion(diff="your git diff here", llm="codex")
35
+ print(result["analysis"]["summary"])
36
+
37
+ # Ask a question
38
+ answer = client.ask(question="Is this SQL query safe?", code="SELECT * FROM users WHERE id = " + user_id)
39
+ print(answer["response"])
40
+
41
+ # Check usage
42
+ status = client.status()
43
+ print(f"Credits: {status['totalAvailable']}")
44
+ ```
45
+
46
+ ## Available Methods
47
+
48
+ | Method | Description | Credits |
49
+ |--------|-------------|---------|
50
+ | `opinion(diff, llm, context)` | Single-model code review | 1 |
51
+ | `ask(question, llm, code, context)` | Ask an AI model a question | 1 |
52
+ | `review(diff, llm, context)` | Code review (alias for opinion) | 1 |
53
+ | `explain(diff, audience)` | Explain changes | 1 |
54
+ | `consensus(diff, context)` | 3-model consensus review | 3 |
55
+ | `bug_hunt(diff, context)` | 3-model bug detection | 3 |
56
+ | `security_audit(diff, context)` | OWASP security scan | 1-3 |
57
+ | `generate_tests(diff, llm, framework)` | Generate tests | 1 |
58
+ | `status()` | Check account credits | 0 |
59
+
60
+ ## Links
61
+
62
+ - [Documentation](https://get2ndopinion.dev/docs)
63
+ - [Pricing](https://get2ndopinion.dev/pricing)
64
+ - [API Reference](https://get2ndopinion.dev/docs/api)
@@ -0,0 +1,49 @@
1
+ # secondopinion
2
+
3
+ Python SDK for the [2ndOpinion](https://get2ndopinion.dev) API — AI-to-AI code review.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install secondopinion-sdk
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```python
14
+ from secondopinion import SecondOpinion
15
+
16
+ client = SecondOpinion(api_key="sk_2op_...")
17
+
18
+ # Get a code review
19
+ result = client.opinion(diff="your git diff here", llm="codex")
20
+ print(result["analysis"]["summary"])
21
+
22
+ # Ask a question
23
+ answer = client.ask(question="Is this SQL query safe?", code="SELECT * FROM users WHERE id = " + user_id)
24
+ print(answer["response"])
25
+
26
+ # Check usage
27
+ status = client.status()
28
+ print(f"Credits: {status['totalAvailable']}")
29
+ ```
30
+
31
+ ## Available Methods
32
+
33
+ | Method | Description | Credits |
34
+ |--------|-------------|---------|
35
+ | `opinion(diff, llm, context)` | Single-model code review | 1 |
36
+ | `ask(question, llm, code, context)` | Ask an AI model a question | 1 |
37
+ | `review(diff, llm, context)` | Code review (alias for opinion) | 1 |
38
+ | `explain(diff, audience)` | Explain changes | 1 |
39
+ | `consensus(diff, context)` | 3-model consensus review | 3 |
40
+ | `bug_hunt(diff, context)` | 3-model bug detection | 3 |
41
+ | `security_audit(diff, context)` | OWASP security scan | 1-3 |
42
+ | `generate_tests(diff, llm, framework)` | Generate tests | 1 |
43
+ | `status()` | Check account credits | 0 |
44
+
45
+ ## Links
46
+
47
+ - [Documentation](https://get2ndopinion.dev/docs)
48
+ - [Pricing](https://get2ndopinion.dev/pricing)
49
+ - [API Reference](https://get2ndopinion.dev/docs/api)
@@ -0,0 +1,25 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "secondopinion-sdk"
7
+ version = "0.1.0"
8
+ description = "Python SDK for the 2ndOpinion API"
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = {text = "MIT"}
12
+ authors = [{name = "2ndOpinion", email = "hello@get2ndopinion.dev"}]
13
+ classifiers = [
14
+ "Programming Language :: Python :: 3",
15
+ "License :: OSI Approved :: MIT License",
16
+ "Operating System :: OS Independent",
17
+ ]
18
+ dependencies = ["httpx>=0.24.0"]
19
+
20
+ [project.urls]
21
+ Homepage = "https://get2ndopinion.dev"
22
+ Repository = "https://github.com/brianmello8/2ndopinion"
23
+
24
+ [tool.hatch.build.targets.wheel]
25
+ packages = ["secondopinion"]
@@ -0,0 +1,4 @@
1
+ from .client import SecondOpinion, SecondOpinionError
2
+
3
+ __all__ = ["SecondOpinion", "SecondOpinionError"]
4
+ __version__ = "0.1.0"
@@ -0,0 +1,176 @@
1
+ """2ndOpinion Python SDK"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ from typing import Any, AsyncIterator, Optional
7
+
8
+ import httpx
9
+
10
+
11
+ class SecondOpinionError(Exception):
12
+ """Error from the 2ndOpinion API."""
13
+
14
+ def __init__(self, message: str, status: int, code: str):
15
+ super().__init__(message)
16
+ self.status = status
17
+ self.code = code
18
+
19
+
20
+ class SecondOpinion:
21
+ """Client for the 2ndOpinion API.
22
+
23
+ Usage::
24
+
25
+ client = SecondOpinion(api_key="sk_2op_...")
26
+ result = client.opinion(diff="diff --git a/foo.ts ...")
27
+ print(result["analysis"]["summary"])
28
+ """
29
+
30
+ def __init__(
31
+ self,
32
+ api_key: str,
33
+ base_url: str = "https://get2ndopinion.dev",
34
+ timeout: float = 45.0,
35
+ max_retries: int = 2,
36
+ ):
37
+ if not api_key:
38
+ raise ValueError("api_key is required")
39
+ self.api_key = api_key
40
+ self.base_url = base_url.rstrip("/")
41
+ self.timeout = timeout
42
+ self.max_retries = max_retries
43
+ self._client = httpx.Client(
44
+ base_url=self.base_url,
45
+ headers={
46
+ "Content-Type": "application/json",
47
+ "X-2ndOpinion-Key": self.api_key,
48
+ },
49
+ timeout=self.timeout,
50
+ )
51
+
52
+ def _request(self, method: str, path: str, **kwargs: Any) -> dict:
53
+ last_error: Optional[Exception] = None
54
+
55
+ for attempt in range(self.max_retries + 1):
56
+ try:
57
+ response = self._client.request(method, path, **kwargs)
58
+
59
+ if response.status_code >= 400:
60
+ body = response.json() if response.headers.get("content-type", "").startswith("application/json") else {}
61
+ error = SecondOpinionError(
62
+ body.get("error", f"HTTP {response.status_code}"),
63
+ response.status_code,
64
+ "RATE_LIMIT" if response.status_code == 429
65
+ else "INSUFFICIENT_CREDITS" if response.status_code == 402
66
+ else "FORBIDDEN" if response.status_code == 403
67
+ else "UNAUTHORIZED" if response.status_code == 401
68
+ else "API_ERROR",
69
+ )
70
+ if response.status_code != 429 and response.status_code < 500:
71
+ raise error
72
+ last_error = error
73
+ continue
74
+
75
+ return response.json()
76
+
77
+ except SecondOpinionError:
78
+ raise
79
+ except Exception as e:
80
+ last_error = e
81
+ if attempt < self.max_retries:
82
+ time.sleep(2**attempt)
83
+
84
+ raise last_error or Exception("Request failed")
85
+
86
+ def opinion(
87
+ self,
88
+ diff: str,
89
+ context: str = "",
90
+ llm: str = "codex",
91
+ ) -> dict:
92
+ """Analyze a git diff for risks and suggestions."""
93
+ return self._request(
94
+ "POST",
95
+ "/api/gateway/opinion",
96
+ json={"diff": diff, "context": context, "llm": llm},
97
+ )
98
+
99
+ def review(
100
+ self,
101
+ diff: str,
102
+ context: str = "",
103
+ llm: str = "codex",
104
+ ) -> dict:
105
+ """Detailed code review of a git diff."""
106
+ return self._request(
107
+ "POST",
108
+ "/api/gateway/review",
109
+ json={"diff": diff, "context": context, "llm": llm},
110
+ )
111
+
112
+ def ask(
113
+ self,
114
+ question: str,
115
+ context: str = "",
116
+ code: str = "",
117
+ llm: str = "codex",
118
+ ) -> dict:
119
+ """Ask a question about code."""
120
+ return self._request(
121
+ "POST",
122
+ "/api/gateway/ask",
123
+ json={"question": question, "context": context, "code": code, "llm": llm},
124
+ )
125
+
126
+ def batch(
127
+ self,
128
+ files: list[dict],
129
+ llm: str = "codex",
130
+ ) -> dict:
131
+ """Analyze multiple files in one request.
132
+
133
+ Args:
134
+ files: List of {"file": "path", "diff": "diff content"}
135
+ llm: LLM to use
136
+ """
137
+ return self._request(
138
+ "POST",
139
+ "/api/gateway/batch",
140
+ json={"files": files, "llm": llm},
141
+ )
142
+
143
+ def status(self) -> dict:
144
+ """Get account status, usage, and available features."""
145
+ return self._request("GET", "/api/gateway/status")
146
+
147
+ def stream(
148
+ self,
149
+ diff: str,
150
+ context: str = "",
151
+ llm: str = "codex",
152
+ ) -> httpx.Response:
153
+ """Stream an analysis response (SSE).
154
+
155
+ Returns an httpx Response that can be iterated::
156
+
157
+ with client.stream(diff="...") as response:
158
+ for line in response.iter_lines():
159
+ if line.startswith("data: "):
160
+ print(line[6:])
161
+ """
162
+ return self._client.stream(
163
+ "POST",
164
+ "/api/gateway/opinion/stream",
165
+ json={"diff": diff, "context": context, "llm": llm},
166
+ )
167
+
168
+ def close(self):
169
+ """Close the HTTP client."""
170
+ self._client.close()
171
+
172
+ def __enter__(self):
173
+ return self
174
+
175
+ def __exit__(self, *args):
176
+ self.close()