Skip to content

Commit 42a7123

Browse files
Copilotsonnyt
andcommitted
Apply Python best practices: custom exceptions, context managers, session pooling, and __repr__ methods
Co-authored-by: sonnyt <183387+sonnyt@users.noreply.github.com>
1 parent ab13227 commit 42a7123

12 files changed

Lines changed: 467 additions & 152 deletions

File tree

README.md

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,29 @@ Official Python SDK for the BundleUp API.
88
pip install bundleup-sdk
99
```
1010

11+
## Features
12+
13+
- **Pythonic API Design** - Context managers, property decorators, and Python best practices
14+
- **Custom Exception Hierarchy** - Specific exception types for different error scenarios
15+
- **Connection Pooling** - Efficient HTTP connection reuse with `requests.Session`
16+
- **Type Hints** - Full type annotation support for better IDE integration
17+
- **Comprehensive Testing** - 70+ unit tests with mocked HTTP requests
18+
1119
## Usage
1220

1321
### Initialize the Client
1422

23+
The SDK supports both regular initialization and context manager usage:
24+
1525
```python
1626
from bundleup import BundleUp
1727

28+
# Regular usage
1829
client = BundleUp("your-api-key")
30+
31+
# Context manager (automatically closes connections)
32+
with BundleUp("your-api-key") as client:
33+
connections = client.connections.list()
1934
```
2035

2136
### Working with Connections
@@ -96,6 +111,12 @@ The Unify API provides a standardized interface across different integrations.
96111
```python
97112
# Get unified chat channels
98113
channels = client.unify("connection-id").chat.channels()
114+
115+
# With pagination parameters
116+
channels = client.unify("connection-id").chat.channels({
117+
"limit": 50,
118+
"include_raw": True
119+
})
99120
```
100121

101122
#### Git
@@ -121,6 +142,68 @@ releases = unify.git.releases()
121142
```python
122143
# Get issues
123144
issues = client.unify("connection-id").pm.issues()
145+
146+
# With pagination
147+
issues = client.unify("connection-id").pm.issues({
148+
"limit": 100,
149+
"after": "cursor-id"
150+
})
151+
```
152+
153+
## Error Handling
154+
155+
The SDK uses a hierarchy of custom exceptions:
156+
157+
```python
158+
from bundleup import BundleUp
159+
from bundleup.exceptions import (
160+
BundleUpError, # Base exception
161+
ValidationError, # Input validation errors
162+
APIError, # General API errors
163+
AuthenticationError, # 401 errors
164+
NotFoundError, # 404 errors
165+
RateLimitError # 429 errors
166+
)
167+
168+
try:
169+
client = BundleUp("your-api-key")
170+
connections = client.connections.list()
171+
except AuthenticationError:
172+
print("Invalid API key")
173+
except NotFoundError:
174+
print("Resource not found")
175+
except RateLimitError:
176+
print("Rate limit exceeded")
177+
except APIError as e:
178+
print(f"API error: {e.status_code} - {e}")
179+
except ValidationError as e:
180+
print(f"Validation error: {e}")
181+
```
182+
183+
## Advanced Usage
184+
185+
### Custom Session
186+
187+
You can provide your own `requests.Session` for advanced configuration:
188+
189+
```python
190+
import requests
191+
from bundleup import BundleUp
192+
193+
session = requests.Session()
194+
session.timeout = 30
195+
session.verify = True
196+
197+
client = BundleUp("your-api-key", session=session)
198+
```
199+
200+
### Query Parameters
201+
202+
The `list()` method supports query parameters:
203+
204+
```python
205+
# List with filters
206+
connections = client.connections.list(status="active", limit=50)
124207
```
125208

126209
## Requirements

bundleup/__init__.py

Lines changed: 44 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,44 +4,68 @@
44
55
Example:
66
>>> from bundleup import BundleUp
7-
>>> client = BundleUp("your-api-key")
8-
>>> connections = client.connections.list()
7+
>>> with BundleUp("your-api-key") as client:
8+
... connections = client.connections.list()
99
"""
1010

11+
from typing import Optional
12+
import requests
13+
1114
from .base import Base
1215
from .connection import Connection, Connections
1316
from .integration import Integration, Integrations
1417
from .webhooks import Webhook, Webhooks
1518
from .proxy import Proxy
1619
from .unify import Unify
1720
from .utils import validate_non_empty_string
21+
from .exceptions import BundleUpError, ValidationError, APIError
1822

1923
__version__ = "0.1.0"
2024

2125

2226
class BundleUp:
23-
"""Main BundleUp SDK client."""
27+
"""Main BundleUp SDK client with context manager support."""
2428

25-
def __init__(self, api_key: str):
29+
def __init__(self, api_key: str, session: Optional[requests.Session] = None):
2630
"""
2731
Initialize the BundleUp client.
2832
2933
Args:
3034
api_key: Your BundleUp API key
35+
session: Optional requests session for connection pooling
3136
3237
Raises:
33-
ValueError: If api_key is not a valid non-empty string
38+
ValidationError: If api_key is not a valid non-empty string
3439
3540
Example:
3641
>>> client = BundleUp("your-api-key")
42+
>>> # or use as context manager
43+
>>> with BundleUp("your-api-key") as client:
44+
... connections = client.connections.list()
3745
"""
3846
validate_non_empty_string(api_key, "api_key")
3947
self._api_key = api_key
48+
self._session = session or requests.Session()
49+
self._owns_session = session is None
4050

41-
# Initialize resource instances
42-
self._connections = Connections(api_key)
43-
self._integrations = Integrations(api_key)
44-
self._webhooks = Webhooks(api_key)
51+
# Initialize resource instances with shared session
52+
self._connections = Connections(api_key, self._session)
53+
self._integrations = Integrations(api_key, self._session)
54+
self._webhooks = Webhooks(api_key, self._session)
55+
56+
def __enter__(self):
57+
"""Enter context manager."""
58+
return self
59+
60+
def __exit__(self, exc_type, exc_val, exc_tb):
61+
"""Exit context manager and cleanup resources."""
62+
self.close()
63+
return False
64+
65+
def close(self):
66+
"""Close the underlying session if owned by this client."""
67+
if self._owns_session and self._session:
68+
self._session.close()
4569

4670
@property
4771
def connections(self) -> Connections:
@@ -93,13 +117,13 @@ def proxy(self, connection_id: str) -> Proxy:
93117
Proxy instance for the specified connection
94118
95119
Raises:
96-
ValueError: If connection_id is not a valid non-empty string
120+
ValidationError: If connection_id is not a valid non-empty string
97121
98122
Example:
99123
>>> proxy = client.proxy("connection-id")
100124
>>> data = proxy.get("/users")
101125
"""
102-
return Proxy(self._api_key, connection_id)
126+
return Proxy(self._api_key, connection_id, self._session)
103127

104128
def unify(self, connection_id: str) -> Unify:
105129
"""
@@ -112,14 +136,18 @@ def unify(self, connection_id: str) -> Unify:
112136
Unify instance for the specified connection
113137
114138
Raises:
115-
ValueError: If connection_id is not a valid non-empty string
139+
ValidationError: If connection_id is not a valid non-empty string
116140
117141
Example:
118142
>>> unify = client.unify("connection-id")
119143
>>> channels = unify.chat.channels()
120144
>>> repos = unify.git.repos()
121145
"""
122-
return Unify(self._api_key, connection_id)
146+
return Unify(self._api_key, connection_id, self._session)
147+
148+
def __repr__(self) -> str:
149+
"""Return a string representation of the client."""
150+
return f"BundleUp(version='{__version__}')"
123151

124152

125153
__all__ = [
@@ -133,4 +161,7 @@ def unify(self, connection_id: str) -> Unify:
133161
"Webhooks",
134162
"Proxy",
135163
"Unify",
164+
"BundleUpError",
165+
"ValidationError",
166+
"APIError",
136167
]

0 commit comments

Comments
 (0)