Skip to content

Commit 762bafe

Browse files
authored
Merge pull request #464 from palewire/main
feat: implement method chaining for create, update, and publish
2 parents 6424242 + c5410ab commit 762bafe

14 files changed

Lines changed: 358 additions & 95 deletions

.clinerules

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1149,6 +1149,104 @@ Comprehensive tests for these operations are in `tests/functional/test_base_char
11491149
- Tests cover successful operations, error cases, and custom access tokens
11501150
- Test functions: `test_base_chart_delete_success`, `test_base_chart_delete_no_chart_id`, `test_base_chart_duplicate_success`, `test_base_chart_duplicate_invalid_response`, `test_base_chart_fork_success`, `test_get_display_urls_success`, `test_get_display_urls_no_chart_id`, `test_get_display_urls_custom_token`, `test_get_iframe_code_success`, `test_get_iframe_code_responsive`, `test_get_iframe_code_no_chart_id`, `test_get_iframe_code_custom_token`, `test_get_editor_url_success`, `test_get_editor_url_no_chart_id`, `test_get_png_url_success`, `test_get_png_url_no_chart_id`, etc.
11511151

1152+
### Method Chaining Pattern (v2.0.0+)
1153+
1154+
**BREAKING CHANGE**: Starting in version 2.0.0, the `create()`, `update()`, and `publish()` methods in BaseChart return `self` instead of their previous return types, enabling method chaining (fluent interface pattern).
1155+
1156+
**Return Type Changes:**
1157+
- `create()`: Changed from `str` (chart_id) to `"BaseChart"` (self)
1158+
- `update()`: Changed from `str` (chart_id) to `"BaseChart"` (self)
1159+
- `publish()`: Changed from `bool` to `"BaseChart"` (self), now raises exception on failure instead of returning False
1160+
1161+
**Key Features:**
1162+
- All three methods now return the chart instance (`self`) for method chaining
1163+
- Methods can be chained together: `chart.create().update().publish()`
1164+
- The `chart_id` is still set internally and accessible via `chart.chart_id`
1165+
- `publish()` now raises an exception on failure instead of returning False
1166+
1167+
**Migration Guide:**
1168+
1169+
*Before (v1.x):*
1170+
```python
1171+
from datawrapper.charts import LineChart
1172+
1173+
chart = LineChart(title="Temperature Data")
1174+
chart_id = chart.create() # Returns chart_id string
1175+
print(f"Created chart: {chart_id}")
1176+
1177+
chart.title = "Updated Temperature Data"
1178+
chart_id = chart.update() # Returns chart_id string
1179+
1180+
success = chart.publish() # Returns bool
1181+
if success:
1182+
print("Published successfully")
1183+
```
1184+
1185+
*After (v2.0.0+):*
1186+
```python
1187+
from datawrapper.charts import LineChart
1188+
1189+
# Method chaining approach
1190+
chart = LineChart(title="Temperature Data").create().publish()
1191+
print(f"Created chart: {chart.chart_id}")
1192+
1193+
# Or step-by-step with chaining
1194+
chart = LineChart(title="Temperature Data")
1195+
chart.create().update().publish()
1196+
1197+
# publish() now raises exception on failure
1198+
try:
1199+
chart.publish()
1200+
except Exception as e:
1201+
print(f"Publish failed: {e}")
1202+
```
1203+
1204+
**Benefits:**
1205+
- More concise code with method chaining
1206+
- Better developer experience with fluent interface
1207+
- Consistent with modern Python API design patterns
1208+
- Enables complex workflows in fewer lines of code
1209+
1210+
**Example Workflows:**
1211+
1212+
```python
1213+
from datawrapper.charts import LineChart, Line
1214+
1215+
# Create, configure, and publish in one chain
1216+
chart = (
1217+
LineChart(title="Sales Trends")
1218+
.create()
1219+
.publish()
1220+
)
1221+
1222+
# Create, update multiple times, then publish
1223+
chart = LineChart(title="Initial Title")
1224+
chart.create()
1225+
chart.title = "Updated Title"
1226+
chart.update()
1227+
chart.description = "Added description"
1228+
chart.update().publish()
1229+
1230+
# Duplicate and immediately publish
1231+
original = LineChart.get(chart_id="abc123")
1232+
duplicate = original.duplicate().publish()
1233+
1234+
# Fork and update in one chain
1235+
fork = original.fork()
1236+
fork.title = "Forked Chart"
1237+
fork.update().publish()
1238+
```
1239+
1240+
**Testing:**
1241+
Comprehensive tests for method chaining are in `tests/functional/test_base_chart_operations.py`:
1242+
- Tests verify that methods return the chart instance (self) for chaining
1243+
- Tests verify identity checks (`result is chart`)
1244+
- Tests cover full chaining workflows: `create().update().publish()`
1245+
- All tests use fully mocked API calls (no API token required)
1246+
1247+
**Documentation:**
1248+
See `docs/user-guide/chart-operations.md` for complete migration guide and examples.
1249+
11521250
## Testing Patterns
11531251

11541252
### Mocking API Calls

datawrapper/charts/base.py

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -615,7 +615,7 @@ def get(cls, chart_id: str, access_token: str | None = None) -> "BaseChart":
615615

616616
def create(
617617
self, access_token: str | None = None, folder_id: int | None = None
618-
) -> str:
618+
) -> "BaseChart":
619619
"""Create a new chart via the Datawrapper API.
620620
621621
Args:
@@ -624,7 +624,7 @@ def create(
624624
folder_id: Optional folder ID to create the chart in.
625625
626626
Returns:
627-
The chart ID of the created chart.
627+
Self, to enable method chaining. The chart ID is stored in self.chart_id.
628628
629629
Raises:
630630
ValueError: If no access token is available or API returns invalid response.
@@ -655,19 +655,19 @@ def create(
655655
if not chart_id or not isinstance(chart_id, str):
656656
raise ValueError(f"Invalid chart ID received from API: {chart_id}")
657657

658-
# Store and return the chart ID
658+
# Store the chart ID and return self for chaining
659659
self.chart_id = chart_id
660-
return self.chart_id
660+
return self
661661

662-
def update(self, access_token: str | None = None) -> str:
662+
def update(self, access_token: str | None = None) -> "BaseChart":
663663
"""Update an existing chart via the Datawrapper API.
664664
665665
Args:
666666
access_token: Optional Datawrapper API access token.
667667
If not provided, will use DATAWRAPPER_ACCESS_TOKEN environment variable.
668668
669669
Returns:
670-
The chart ID of the updated chart.
670+
Self, to enable method chaining.
671671
672672
Raises:
673673
ValueError: If no chart_id is set or no access token is available.
@@ -695,25 +695,25 @@ def update(self, access_token: str | None = None) -> str:
695695
metadata=metadata["metadata"],
696696
)
697697

698-
# Return the chart ID
699-
return self.chart_id
698+
# Return self for chaining
699+
return self
700700

701701
def publish(
702702
self,
703703
access_token: str | None = None,
704-
) -> bool:
704+
) -> "BaseChart":
705705
"""Publish the chart via the Datawrapper API.
706706
707707
Args:
708708
access_token: Optional Datawrapper API access token.
709709
If not provided, will use DATAWRAPPER_ACCESS_TOKEN environment variable.
710710
711711
Returns:
712-
True if the chart was published successfully, False otherwise.
712+
Self, to enable method chaining.
713713
714714
Raises:
715715
ValueError: If no chart_id is set or no access token is available.
716-
Exception: If the API request fails.
716+
Exception: If the API request fails or publishing fails.
717717
"""
718718
if not self.chart_id:
719719
raise ValueError(
@@ -726,8 +726,12 @@ def publish(
726726
# Call the publish_chart method from the client
727727
result = client.publish_chart(chart_id=self.chart_id)
728728

729-
# Return a boolean indicating success
730-
return True if result else False
729+
# Raise an exception if publishing failed
730+
if not result:
731+
raise Exception(f"Failed to publish chart {self.chart_id}")
732+
733+
# Return self for chaining
734+
return self
731735

732736
def export(
733737
self,
Lines changed: 17 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Working with chart objects
22

3-
This guide tours the key operations available on all of our object-oriented chart types.
3+
This guide tours the key operations available on all charts based on our object-oriented classes.
44

55
## Creating a new chart
66

@@ -16,20 +16,20 @@ data = pd.DataFrame(
1616
}
1717
)
1818

19-
# Create a chart
19+
# Configure the chart object
2020
chart = dw.BarChart(
2121
title="Most Popular Programming Languages 2024",
2222
data=data,
2323
value_label_format=dw.NumberFormat.ONE_DECIMAL,
2424
)
2525

26-
# Create it in Datawrapper
27-
chart_id = chart.create()
26+
# Create it by sending to Datawrapper
27+
chart.create()
2828
```
2929

3030
## Getting an existing chart
3131

32-
You can retrieve an existing chart by using `get_chart` with the chart ID:
32+
You can retrieve an existing chart with the ID, which is found in its URL.
3333

3434
```python
3535
# Retrieve an existing chart
@@ -42,8 +42,6 @@ print(existing_chart.chart_id)
4242

4343
## Updating an existing chart
4444

45-
After creating or retrieving a chart, you can modify its properties and update it:
46-
4745
```python
4846
# Modify the chart properties
4947
chart.title = "Programming Language Popularity - Updated"
@@ -58,7 +56,7 @@ new_data = pd.DataFrame(
5856
)
5957
chart.data = new_data
6058

61-
# Push the changes to Datawrapper
59+
# This will send the updates to Datawrapper
6260
chart.update()
6361
```
6462

@@ -68,12 +66,13 @@ Once your chart is ready, publish it to make it publicly accessible:
6866

6967
```python
7068
chart.publish()
69+
70+
# Chain with other operations
71+
chart.create().publish()
7172
```
7273

7374
## Exporting a chart
7475

75-
Export your chart as an SVG, PNG, or PDF file:
76-
7776
```python
7877
# Export as PNG (default)
7978
chart.export(filepath="chart.png")
@@ -87,14 +86,9 @@ chart.export(
8786
)
8887
```
8988

90-
The export method supports various other options for customizing the output format and dimensions.
91-
9289
## Duplicating a chart
9390

94-
Create an editable copy of your chart:
95-
9691
```python
97-
# Create a duplicate
9892
duplicate_chart = chart.duplicate()
9993

10094
# The duplicate is a new chart with a different ID
@@ -108,16 +102,19 @@ duplicate_chart.update()
108102

109103
## Deleting a chart
110104

111-
Remove a chart from Datawrapper:
112-
113105
```python
114-
# Delete the chart
115106
success = chart.delete()
116107

117108
if success:
118109
print("Chart deleted successfully")
119-
# The chart_id is now None
120-
print(f"Chart ID: {chart.chart_id}")
110+
```
111+
112+
## Getting editor URL
113+
114+
Get the Datawrapper URL to continue editing your chart:
115+
116+
```python
117+
chart.get_editor_url()
121118
```
122119

123120
## Getting iframe code
@@ -127,35 +124,17 @@ Get the HTML iframe embed code for your chart:
127124
```python
128125
# Get standard iframe code
129126
iframe_code = chart.get_iframe_code()
130-
print(iframe_code)
131127

132128
# Get responsive iframe code
133129
responsive_iframe = chart.get_iframe_code(responsive=True)
134-
print(responsive_iframe)
135-
```
136-
137-
The iframe code can be directly embedded in HTML pages. The `responsive=True` option generates code that automatically adjusts to container width.
138-
139-
## Getting editor URL
140-
141-
Get the Datawrapper URL to continue editing your chart:
142-
143-
```python
144-
# Get the editor URL
145-
chart.get_editor_url()
146130
```
147131

148132
## Getting png URL
149133

150134
Get the fallback image URL for use in noscript tags:
151135

152136
```python
153-
# Get the PNG URL
154137
png_url = chart.get_png_url()
155-
print(f"PNG fallback: {png_url}")
156138

157-
# Use in HTML noscript tags
158139
html = f'<noscript><img src="{png_url}" alt="Chart" /></noscript>'
159140
```
160-
161-
This provides a static image fallback for environments where JavaScript is disabled. It's also a handy way to get a direct link to the chart image for other uses.

docs/user-guide/quickstart.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,7 @@ chart = dw.BarChart(
5757
)
5858

5959
# Create the chart on Datawrapper (uses DATAWRAPPER_ACCESS_TOKEN environment variable)
60-
chart_id = chart.create()
61-
print(f"Chart created! ID: {chart_id}")
60+
chart.create()
6261

6362
# You could also provide the token at runtime
6463
# chart_id = chart.create(access_token="your_token_here")
@@ -85,7 +84,7 @@ for category in ["Sales", "Revenue", "Profit"]:
8584
chart = dw.BarChart(
8685
title=f"{category} by Region", data=get_data_for_category(category)
8786
)
88-
chart_id = chart.create()
87+
chart.create()
8988
```
9089

9190
Happy charting! 📊

tests/functional/test_api_end_to_end.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,8 @@ def test_create_sample_bar_chart_with_datawrapper():
123123

124124
# Create the chart via Datawrapper API
125125
print("🚀 Creating chart via Datawrapper API...")
126-
chart_id = chart.create()
126+
chart.create()
127+
chart_id = chart.chart_id
127128

128129
print(f"✅ Chart created successfully with ID: {chart_id}")
129130

@@ -199,7 +200,8 @@ def test_create_simple_bar_chart_with_api():
199200

200201
# Create via API
201202
print("🚀 Creating chart via API...")
202-
chart_id = chart.create()
203+
chart.create()
204+
chart_id = chart.chart_id
203205

204206
print(f"✅ Chart created with ID: {chart_id}")
205207

@@ -341,7 +343,8 @@ def test_create_happiness_scores_bar_chart_with_datawrapper():
341343

342344
# Create the chart via Datawrapper API
343345
print("🚀 Creating chart via Datawrapper API...")
344-
chart_id = chart.create()
346+
chart.create()
347+
chart_id = chart.chart_id
345348

346349
print(f"✅ Chart created successfully with ID: {chart_id}")
347350

0 commit comments

Comments
 (0)