Skip to content

Commit f65f768

Browse files
authored
Merge pull request #20 from paywithextend/development
Release 1.2.0
2 parents fe6aaad + 9011bd2 commit f65f768

File tree

8 files changed

+91
-46
lines changed

8 files changed

+91
-46
lines changed

.github/workflows/release.yml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,10 @@ jobs:
3434
rm -rf build dist *.egg-info
3535
make build ENV=stage
3636
37-
- name: Extract Version from pyproject.toml
37+
- name: Extract Version from version file
3838
id: get_version
3939
run: |
40-
# Extract the version assuming a line like: version = "0.1.0"
41-
VERSION=$(grep -Po '^version\s*=\s*"\K[^"]+' pyproject.toml)
40+
VERSION=$(grep -Po '^__version__\s*=\s*"\K[^"]+' extend/__version__.py)
4241
echo "Version extracted: $VERSION"
4342
echo "version=$VERSION" >> $GITHUB_OUTPUT
4443

extend/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
"""
44

55
from extend.models import VirtualCard, Transaction, RecurrenceConfig
6+
from .__version__ import __version__ as _version
67
from .extend import ExtendClient
78

8-
__version__ = "1.1.0"
9+
__version__ = _version
910

1011
__all__ = [
1112
"ExtendClient",

extend/__version__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
__version__ = "1.2.0"

extend/client.py

Lines changed: 27 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -66,15 +66,7 @@ async def get(self, url: str, params: Optional[Dict] = None) -> Any:
6666
httpx.HTTPError: If the request fails
6767
ValueError: If the response is not valid JSON
6868
"""
69-
async with httpx.AsyncClient() as client:
70-
response = await client.get(
71-
self.build_full_url(url),
72-
headers=self.headers,
73-
params=params,
74-
timeout=httpx.Timeout(30)
75-
)
76-
response.raise_for_status()
77-
return response.json()
69+
return await self._send_request("GET", url, params=params)
7870

7971
async def post(self, url: str, data: Dict) -> Any:
8072
"""Make a POST request to the Extend API.
@@ -90,15 +82,7 @@ async def post(self, url: str, data: Dict) -> Any:
9082
httpx.HTTPError: If the request fails
9183
ValueError: If the response is not valid JSON
9284
"""
93-
async with httpx.AsyncClient() as client:
94-
response = await client.post(
95-
self.build_full_url(url),
96-
headers=self.headers,
97-
json=data,
98-
timeout=httpx.Timeout(30)
99-
)
100-
response.raise_for_status()
101-
return response.json()
85+
return await self._send_request("POST", url, json=data)
10286

10387
async def put(self, url: str, data: Dict) -> Any:
10488
"""Make a PUT request to the Extend API.
@@ -114,15 +98,7 @@ async def put(self, url: str, data: Dict) -> Any:
11498
httpx.HTTPError: If the request fails
11599
ValueError: If the response is not valid JSON
116100
"""
117-
async with httpx.AsyncClient() as client:
118-
response = await client.put(
119-
self.build_full_url(url),
120-
headers=self.headers,
121-
json=data,
122-
timeout=httpx.Timeout(30)
123-
)
124-
response.raise_for_status()
125-
return response.json()
101+
return await self._send_request("PUT", url, json=data)
126102

127103
async def patch(self, url: str, data: Dict) -> Any:
128104
"""Make a PATCH request to the Extend API.
@@ -138,15 +114,7 @@ async def patch(self, url: str, data: Dict) -> Any:
138114
httpx.HTTPError: If the request fails
139115
ValueError: If the response is not valid JSON
140116
"""
141-
async with httpx.AsyncClient() as client:
142-
response = await client.patch(
143-
self.build_full_url(url),
144-
headers=self.headers,
145-
json=data,
146-
timeout=httpx.Timeout(30)
147-
)
148-
response.raise_for_status()
149-
return response.json()
117+
return await self._send_request("PATCH", url, json=data)
150118

151119
async def post_multipart(
152120
self,
@@ -173,16 +141,34 @@ async def post_multipart(
173141
"""
174142
# When sending multipart data, we pass `data` (for non-file fields)
175143
# and `files` (for file uploads) separately.
144+
return await self._send_request("POST", url, data=data, files=files)
145+
146+
def build_full_url(self, url: Optional[str]):
147+
return f"https://{API_HOST}{url or ''}"
148+
149+
async def _send_request(
150+
self,
151+
method: str,
152+
url: str,
153+
*,
154+
params: Optional[Dict] = None,
155+
json: Optional[Dict] = None,
156+
data: Optional[Dict] = None,
157+
files: Optional[Dict] = None
158+
) -> Any:
176159
async with httpx.AsyncClient() as client:
177-
response = await client.post(
178-
self.build_full_url(url),
160+
response = await client.request(
161+
method=method.upper(),
162+
url=self.build_full_url(url),
179163
headers=self.headers,
164+
params=params,
165+
json=json,
180166
data=data,
181167
files=files,
182168
timeout=httpx.Timeout(30)
183169
)
184170
response.raise_for_status()
185-
return response.json()
186171

187-
def build_full_url(self, url: Optional[str]):
188-
return f"https://{API_HOST}{url or ''}"
172+
if response.content:
173+
return response.json()
174+
return None

extend/resources/transactions.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,3 +114,20 @@ async def update_transaction_expense_data(self, transaction_id: str, data: Dict)
114114
path=f"/{transaction_id}/expensedata",
115115
params=data
116116
)
117+
118+
async def send_receipt_reminder(self, transaction_id: str) -> Dict:
119+
"""Send a transaction-specific receipt reminder.
120+
121+
Args:
122+
transaction_id (str): The unique identifier of the transaction.
123+
124+
Returns:
125+
None
126+
127+
Raises:
128+
httpx.HTTPError: If the request fails.
129+
"""
130+
return await self._request(
131+
method="post",
132+
path=f"/{transaction_id}/receiptreminder"
133+
)

pyproject.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ version = "1.1.0"
88
description = "Python client for the Extend API"
99
readme = "README.md"
1010
authors = [{ name = "Extend Engineering", email = "[email protected]" }]
11+
dynamic = ["version"]
1112
license = { text = "MIT" }
1213
classifiers = [
1314
"Programming Language :: Python :: 3",
@@ -26,6 +27,9 @@ dependencies = [
2627
"Issue Tracker" = "https://github.com/paywithextend/extend-python/issues"
2728
"Source Code" = "https://github.com/paywithextend/extend-python"
2829

30+
[tool.hatch.version]
31+
path = "extend/__version__.py"
32+
2933
[tool.hatch.build.targets.wheel]
3034
packages = ["extend"]
3135

tests/test_client.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -649,3 +649,17 @@ async def test_get_automatch_status(extend, mocker):
649649
assert response["id"] == "job_123"
650650
assert len(response["tasks"]) == 1
651651
assert response["tasks"][0]["transactionId"] == "txn_123"
652+
653+
654+
@pytest.mark.asyncio
655+
async def test_send_receipt_reminder(extend, mocker, mock_transaction):
656+
mock_post = mocker.patch.object(
657+
extend._api_client,
658+
'post',
659+
return_value=None
660+
)
661+
662+
result = await extend.transactions.send_receipt_reminder(mock_transaction["id"])
663+
664+
assert result is None
665+
mock_post.assert_called_once_with("/transactions/txn_123/receiptreminder", None)

tests/test_integration.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -432,7 +432,7 @@ async def test_update_transaction_expense_data_with_specific_category_and_label(
432432
assert label_resp["code"] == label_code
433433

434434
# Retrieve at least one transaction to update expense data
435-
transactions_response = await extend.transactions.get_transactions(per_page=1)
435+
transactions_response = await extend.transactions.get_transactions(per_page=1, sort_field="-date")
436436
assert "report" in transactions_response, "Response should include 'report'"
437437
assert "transactions" in transactions_response["report"], "Response should include 'transactions'"
438438
assert transactions_response["report"][
@@ -534,6 +534,29 @@ async def test_automatch_receipts_and_get_status(self, extend):
534534
assert status_response["id"] == job_id, "Job id should match the one returned during automatch"
535535
assert "tasks" in status_response, "Status response should include tasks"
536536

537+
@pytest.mark.asyncio
538+
async def test_send_receipt_reminder(self, extend):
539+
"""Test sending a receipt reminder for a transaction that requires a receipt."""
540+
541+
# Fetch a page of transactions and look for one that requires a receipt
542+
response = await extend.transactions.get_transactions(per_page=20, sort_field='-date')
543+
transactions = response.get("report", {}).get("transactions", [])
544+
545+
# Find a transaction with receiptRequired = True
546+
tx_with_receipt_required = next(
547+
(tx for tx in transactions if tx.get("receiptRequired") is True),
548+
None
549+
)
550+
551+
assert tx_with_receipt_required, "No transactions found with receiptRequired = True"
552+
transaction_id = tx_with_receipt_required["id"]
553+
554+
# Send receipt reminder
555+
result = await extend.transactions.send_receipt_reminder(transaction_id)
556+
557+
# The call should succeed and return None
558+
assert result is None
559+
537560

538561
def test_environment_variables():
539562
"""Test that required environment variables are set"""

0 commit comments

Comments
 (0)