Skip to content

Commit 47b58fa

Browse files
authored
Merge pull request #12 from franccesco/feat/dx-improvements
feat!: improve developer experience with breaking API changes (v0.20.0)
2 parents 897fecb + 42f2351 commit 47b58fa

38 files changed

Lines changed: 865 additions & 274 deletions

CHANGELOG.md

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.20.0] - 2025-12-10
11+
12+
### Added
13+
14+
- `client.issue.update()` method for updating existing issues
15+
- `client.scorecard.get()` method for retrieving scorecard details
16+
- `GoalStatus` enum for type-safe status values
17+
- `base_url` and `timeout` parameters to sync `Client` for configuration flexibility
18+
- AsyncClient now validates API key at initialization
19+
20+
### Changed
21+
22+
- **BREAKING**: `client.user.all()` renamed to `client.user.list()` for consistency
23+
- **BREAKING**: `client.user.details(all=True)` renamed to `client.user.details(include_all=True)` for clarity
24+
- **BREAKING**: `client.issue.solve()` renamed to `client.issue.complete()` and now returns `IssueDetails`
25+
- **BREAKING**: `client.todo.complete()` now returns `Todo` instead of `bool`
26+
- **BREAKING**: `client.goal.update/delete/archive/restore()` now return `None` instead of `bool`
27+
- **BREAKING**: `client.headline.update/delete()` now return `None` instead of `bool`
28+
- **BREAKING**: Client raises `ConfigurationError` instead of `ValueError` for missing API key
29+
30+
### Fixed
31+
32+
- All documentation updated to reflect the breaking changes
33+
1034
## [0.19.0] - 2025-12-10
1135

1236
### Fixed
@@ -192,7 +216,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
192216
- Configuration management with multiple API key sources
193217
- httpx-based HTTP client with bearer token authentication
194218

195-
[Unreleased]: https://github.com/franccesco/bloomy-python/compare/v0.19.0...HEAD
219+
[Unreleased]: https://github.com/franccesco/bloomy-python/compare/v0.20.0...HEAD
220+
[0.20.0]: https://github.com/franccesco/bloomy-python/compare/v0.19.0...v0.20.0
196221
[0.19.0]: https://github.com/franccesco/bloomy-python/compare/v0.18.0...v0.19.0
197222
[0.18.0]: https://github.com/franccesco/bloomy-python/compare/v0.17.0...v0.18.0
198223
[0.17.0]: https://github.com/franccesco/bloomy-python/compare/v0.16.0...v0.17.0

docs/api/async_client.md

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,55 @@
1010
- __aenter__
1111
- __aexit__
1212
- close
13-
heading_level: 2
13+
heading_level: 2
14+
15+
## Basic Usage
16+
17+
```python
18+
import asyncio
19+
from bloomy import AsyncClient, ConfigurationError
20+
21+
async def main():
22+
# Initialize with API key
23+
async with AsyncClient(api_key="your-api-key") as client:
24+
users = await client.user.list()
25+
26+
# Custom base URL (for testing/staging)
27+
async with AsyncClient(
28+
api_key="your-api-key",
29+
base_url="https://staging.example.com/api/v1"
30+
) as client:
31+
users = await client.user.list()
32+
33+
# Custom timeout (in seconds)
34+
async with AsyncClient(api_key="your-api-key", timeout=60.0) as client:
35+
users = await client.user.list()
36+
37+
# Handle missing API key
38+
try:
39+
client = AsyncClient()
40+
except ConfigurationError as e:
41+
print(f"Configuration error: {e}")
42+
43+
asyncio.run(main())
44+
```
45+
46+
## Parameters
47+
48+
- **api_key** (str, optional): Your Bloom Growth API key. If not provided, will attempt to load from `BG_API_KEY` environment variable or `~/.bloomy/config.yaml`
49+
- **base_url** (str, optional): Custom API endpoint. Defaults to `"https://app.bloomgrowth.com/api/v1"`
50+
- **timeout** (float, optional): Request timeout in seconds. Defaults to `30.0`
51+
52+
## Exceptions
53+
54+
- **ConfigurationError**: Raised when no API key can be found from any source
55+
56+
## Context Manager
57+
58+
The AsyncClient supports async context manager protocol for automatic resource cleanup:
59+
60+
```python
61+
async with AsyncClient(api_key="your-api-key") as client:
62+
users = await client.user.list()
63+
# Client automatically closes when exiting the context
64+
```

docs/api/client.md

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,48 @@ The main entry point for interacting with the Bloomy API.
88
show_root_heading: true
99
show_root_full_path: false
1010
members_order: source
11-
show_signature_annotations: true
11+
show_signature_annotations: true
12+
13+
## Basic Usage
14+
15+
```python
16+
from bloomy import Client, ConfigurationError
17+
18+
# Initialize with API key
19+
client = Client(api_key="your-api-key")
20+
21+
# Custom base URL (for testing/staging)
22+
client = Client(
23+
api_key="your-api-key",
24+
base_url="https://staging.example.com/api/v1"
25+
)
26+
27+
# Custom timeout (in seconds)
28+
client = Client(api_key="your-api-key", timeout=60.0)
29+
30+
# Handle missing API key
31+
try:
32+
client = Client()
33+
except ConfigurationError as e:
34+
print(f"Configuration error: {e}")
35+
```
36+
37+
## Parameters
38+
39+
- **api_key** (str, optional): Your Bloom Growth API key. If not provided, will attempt to load from `BG_API_KEY` environment variable or `~/.bloomy/config.yaml`
40+
- **base_url** (str, optional): Custom API endpoint. Defaults to `"https://app.bloomgrowth.com/api/v1"`
41+
- **timeout** (float, optional): Request timeout in seconds. Defaults to `30.0`
42+
43+
## Exceptions
44+
45+
- **ConfigurationError**: Raised when no API key can be found from any source
46+
47+
## Context Manager
48+
49+
The Client supports context manager protocol for automatic resource cleanup:
50+
51+
```python
52+
with Client(api_key="your-api-key") as client:
53+
users = client.user.list()
54+
# Client automatically closes when exiting the context
55+
```

docs/api/operations/goals.md

Lines changed: 52 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -28,81 +28,106 @@ The async version `AsyncGoalOperations` provides the same methods as above, but
2828
!!! info "Async Usage"
2929
All methods have the same parameters and return types as their sync counterparts. Simply add `await` before each method call.
3030

31+
## Goal Status Enum
32+
33+
The SDK provides a `GoalStatus` enum for type-safe status updates:
34+
35+
```python
36+
from bloomy import GoalStatus
37+
38+
# Enum values (recommended)
39+
GoalStatus.ON_TRACK # "on" - Goal is on track
40+
GoalStatus.AT_RISK # "off" - Goal is at risk
41+
GoalStatus.COMPLETE # "complete" - Goal is complete
42+
43+
# You can still use strings directly
44+
client.goal.update(goal_id=123, status="on")
45+
```
46+
47+
!!! tip "Using the Enum"
48+
Using `GoalStatus` enum values is recommended for better type checking and IDE autocomplete support.
49+
3150
## Usage Examples
3251

3352
=== "Sync"
3453

3554
```python
36-
from bloomy import Client
37-
55+
from bloomy import Client, GoalStatus
56+
3857
with Client(api_key="your-api-key") as client:
3958
# List active goals
4059
goals = client.goal.list()
4160
for goal in goals:
4261
print(f"{goal.title} - Status: {goal.status}")
43-
62+
4463
# List with archived goals included
4564
all_goals = client.goal.list(archived=True)
4665
print(f"Active: {len(all_goals.active)}")
4766
print(f"Archived: {len(all_goals.archived)}")
48-
67+
4968
# Create a new goal
5069
new_goal = client.goal.create(
5170
title="Increase customer retention by 20%",
5271
meeting_id=123
5372
)
54-
55-
# Update goal status
73+
74+
# Update goal status using enum (recommended)
5675
client.goal.update(
5776
goal_id=new_goal.id,
58-
status="on" # on track
77+
status=GoalStatus.ON_TRACK
5978
)
60-
61-
# Archive and restore
79+
80+
# Or use string directly
81+
client.goal.update(goal_id=new_goal.id, status="off") # at risk
82+
83+
# Archive and restore (both return None)
6284
client.goal.archive(goal_id=new_goal.id)
6385
client.goal.restore(goal_id=new_goal.id)
64-
65-
# Delete a goal
86+
87+
# Delete a goal (returns None)
6688
client.goal.delete(goal_id=new_goal.id)
6789
```
6890

6991
=== "Async"
7092

7193
```python
7294
import asyncio
73-
from bloomy import AsyncClient
74-
95+
from bloomy import AsyncClient, GoalStatus
96+
7597
async def main():
7698
async with AsyncClient(api_key="your-api-key") as client:
7799
# List active goals
78100
goals = await client.goal.list()
79101
for goal in goals:
80102
print(f"{goal.title} - Status: {goal.status}")
81-
103+
82104
# List with archived goals included
83105
all_goals = await client.goal.list(archived=True)
84106
print(f"Active: {len(all_goals.active)}")
85107
print(f"Archived: {len(all_goals.archived)}")
86-
108+
87109
# Create a new goal
88110
new_goal = await client.goal.create(
89111
title="Increase customer retention by 20%",
90112
meeting_id=123
91113
)
92-
93-
# Update goal status
114+
115+
# Update goal status using enum (recommended)
94116
await client.goal.update(
95117
goal_id=new_goal.id,
96-
status="on" # on track
118+
status=GoalStatus.ON_TRACK
97119
)
98-
99-
# Archive and restore
120+
121+
# Or use string directly
122+
await client.goal.update(goal_id=new_goal.id, status="off") # at risk
123+
124+
# Archive and restore (both return None)
100125
await client.goal.archive(goal_id=new_goal.id)
101126
await client.goal.restore(goal_id=new_goal.id)
102-
103-
# Delete a goal
127+
128+
# Delete a goal (returns None)
104129
await client.goal.delete(goal_id=new_goal.id)
105-
130+
106131
asyncio.run(main())
107132
```
108133

@@ -118,4 +143,7 @@ The async version `AsyncGoalOperations` provides the same methods as above, but
118143
| `restore()` | Restore an archived goal | `goal_id` |
119144

120145
!!! info "Status Values"
121-
Valid status values are: `'on'` (On Track), `'off'` (At Risk), or `'complete'` (Completed)
146+
Valid status values are: `'on'` (On Track), `'off'` (At Risk), or `'complete'` (Completed). Use the `GoalStatus` enum for type-safe updates.
147+
148+
!!! note "Return Values"
149+
The `update()`, `delete()`, `archive()`, and `restore()` methods return `None` instead of boolean values.

docs/api/operations/headlines.md

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,13 @@ The async version `AsyncHeadlineOperations` provides the same methods as above,
5454
# List headlines for a specific meeting
5555
meeting_headlines = client.headline.list(meeting_id=123)
5656
57-
# Update headline title
57+
# Update headline title (returns None)
5858
client.headline.update(
5959
headline_id=headline.id,
6060
title="Product launch exceeded expectations"
6161
)
62-
63-
# Delete headline
62+
63+
# Delete headline (returns None)
6464
client.headline.delete(headline_id=headline.id)
6565
```
6666

@@ -90,13 +90,13 @@ The async version `AsyncHeadlineOperations` provides the same methods as above,
9090
# List headlines for a specific meeting
9191
meeting_headlines = await client.headline.list(meeting_id=123)
9292
93-
# Update headline title
93+
# Update headline title (returns None)
9494
await client.headline.update(
9595
headline_id=headline.id,
9696
title="Product launch exceeded expectations"
9797
)
98-
99-
# Delete headline
98+
99+
# Delete headline (returns None)
100100
await client.headline.delete(headline_id=headline.id)
101101

102102
asyncio.run(main())
@@ -113,4 +113,7 @@ The async version `AsyncHeadlineOperations` provides the same methods as above,
113113
| `delete()` | Delete a headline | `headline_id` |
114114

115115
!!! note "Filtering"
116-
Like todos, headlines can be filtered by either `user_id` or `meeting_id`, but not both.
116+
Like todos, headlines can be filtered by either `user_id` or `meeting_id`, but not both.
117+
118+
!!! note "Return Values"
119+
The `update()` and `delete()` methods return `None` instead of boolean values.

0 commit comments

Comments
 (0)