Add typed Pydantic models for positions and document payments, enhance EasybillClient with pagination and retry handling, and implement unit tests for workflow helpers.
This commit is contained in:
parent
b324671286
commit
57d6a49986
6 changed files with 375 additions and 5 deletions
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"forgejo.preferredRemote": "forgejo"
|
||||||
|
}
|
||||||
16
README.md
16
README.md
|
|
@ -9,6 +9,8 @@ The initial implementation is in place and includes:
|
||||||
- a project scaffold with packaging and tests
|
- a project scaffold with packaging and tests
|
||||||
- sync and async wrapper clients
|
- sync and async wrapper clients
|
||||||
- authentication helpers for bearer and basic auth
|
- authentication helpers for bearer and basic auth
|
||||||
|
- typed Pydantic facades for customers, documents, positions, and document payments
|
||||||
|
- pagination helpers and basic retry handling for HTTP 429 rate limits
|
||||||
- a webhook parser for JSON and form payloads
|
- a webhook parser for JSON and form payloads
|
||||||
- a reproducible generation script based on the provided Swagger specification
|
- a reproducible generation script based on the provided Swagger specification
|
||||||
|
|
||||||
|
|
@ -26,6 +28,20 @@ Generate the raw clients from the API description:
|
||||||
python scripts/generate_client.py --mode both
|
python scripts/generate_client.py --mode both
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```python
|
||||||
|
from easybill_client import EasybillClient
|
||||||
|
|
||||||
|
with EasybillClient(api_key="YOUR_API_TOKEN", max_retries=2, retry_backoff=1.0) as client:
|
||||||
|
customer_page = client.list_customers(limit=50)
|
||||||
|
position = client.get_position(5)
|
||||||
|
payment = client.create_document_payment(document_id=10, amount=1999, reference="INV-10")
|
||||||
|
|
||||||
|
for customer in client.iter_all_customers(limit=100):
|
||||||
|
print(customer.id, customer.company_name)
|
||||||
|
```
|
||||||
|
|
||||||
## Structure
|
## Structure
|
||||||
|
|
||||||
- `src/easybill_client`: public package and middleware-friendly helpers
|
- `src/easybill_client`: public package and middleware-friendly helpers
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,19 @@
|
||||||
from .auth import EasybillAuth, basic_auth_header, bearer_auth_header
|
from .auth import EasybillAuth, basic_auth_header, bearer_auth_header
|
||||||
from .client import AsyncEasybillClient, EasybillClient
|
from .client import AsyncEasybillClient, EasybillClient
|
||||||
from .models import Customer, Document, PagedResult, Pagination, WebhookEnvelope, WebhookSubscription
|
from .models import Customer, Document, DocumentPayment, PagedResult, Pagination, Position, WebhookEnvelope, WebhookSubscription
|
||||||
from .webhooks import EasybillWebhookParser
|
from .webhooks import EasybillWebhookParser
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"AsyncEasybillClient",
|
"AsyncEasybillClient",
|
||||||
"Customer",
|
"Customer",
|
||||||
"Document",
|
"Document",
|
||||||
|
"DocumentPayment",
|
||||||
"EasybillAuth",
|
"EasybillAuth",
|
||||||
"EasybillClient",
|
"EasybillClient",
|
||||||
"EasybillWebhookParser",
|
"EasybillWebhookParser",
|
||||||
"PagedResult",
|
"PagedResult",
|
||||||
"Pagination",
|
"Pagination",
|
||||||
|
"Position",
|
||||||
"WebhookEnvelope",
|
"WebhookEnvelope",
|
||||||
"WebhookSubscription",
|
"WebhookSubscription",
|
||||||
"basic_auth_header",
|
"basic_auth_header",
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import time
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
|
||||||
from .auth import EasybillAuth
|
from .auth import EasybillAuth
|
||||||
from .models import Customer, Document, PagedResult, WebhookSubscription
|
from .models import Customer, Document, DocumentPayment, EasybillBaseModel, PagedResult, Position, WebhookSubscription
|
||||||
|
|
||||||
|
|
||||||
class EasybillAPIError(RuntimeError):
|
class EasybillAPIError(RuntimeError):
|
||||||
|
|
@ -26,12 +28,16 @@ class _BaseEasybillClient:
|
||||||
api_prefix: str = "/rest/v1",
|
api_prefix: str = "/rest/v1",
|
||||||
timeout: float = 30.0,
|
timeout: float = 30.0,
|
||||||
default_limit: int = 100,
|
default_limit: int = 100,
|
||||||
|
max_retries: int = 0,
|
||||||
|
retry_backoff: float = 1.0,
|
||||||
transport: httpx.BaseTransport | httpx.AsyncBaseTransport | None = None,
|
transport: httpx.BaseTransport | httpx.AsyncBaseTransport | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.base_url = base_url.rstrip("/")
|
self.base_url = base_url.rstrip("/")
|
||||||
self.api_prefix = api_prefix.rstrip("/")
|
self.api_prefix = api_prefix.rstrip("/")
|
||||||
self.timeout = timeout
|
self.timeout = timeout
|
||||||
self.default_limit = default_limit
|
self.default_limit = default_limit
|
||||||
|
self.max_retries = max_retries
|
||||||
|
self.retry_backoff = retry_backoff
|
||||||
self.transport = transport
|
self.transport = transport
|
||||||
self.auth = EasybillAuth(api_key, email=email, use_basic_auth=use_basic_auth)
|
self.auth = EasybillAuth(api_key, email=email, use_basic_auth=use_basic_auth)
|
||||||
self.default_headers = {
|
self.default_headers = {
|
||||||
|
|
@ -68,7 +74,7 @@ class _BaseEasybillClient:
|
||||||
return cleaned or None
|
return cleaned or None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _parse_paged_result(data: dict[str, Any], item_model: type[Customer] | type[Document] | type[WebhookSubscription]) -> PagedResult:
|
def _parse_paged_result(data: dict[str, Any], item_model: type[EasybillBaseModel]) -> PagedResult:
|
||||||
items = [item_model.model_validate(item) for item in data.get("items", [])]
|
items = [item_model.model_validate(item) for item in data.get("items", [])]
|
||||||
return PagedResult(
|
return PagedResult(
|
||||||
page=data.get("page", 1),
|
page=data.get("page", 1),
|
||||||
|
|
@ -78,6 +84,15 @@ class _BaseEasybillClient:
|
||||||
items=items,
|
items=items,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _get_retry_delay(self, response: httpx.Response, attempt: int) -> float:
|
||||||
|
retry_after = response.headers.get("Retry-After")
|
||||||
|
if retry_after is not None:
|
||||||
|
try:
|
||||||
|
return max(0.0, float(retry_after))
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
return max(0.0, self.retry_backoff * attempt)
|
||||||
|
|
||||||
|
|
||||||
class EasybillClient(_BaseEasybillClient):
|
class EasybillClient(_BaseEasybillClient):
|
||||||
def __init__(self, api_key: str, **kwargs: Any) -> None:
|
def __init__(self, api_key: str, **kwargs: Any) -> None:
|
||||||
|
|
@ -100,7 +115,17 @@ class EasybillClient(_BaseEasybillClient):
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
def request(self, method: str, path: str, **kwargs: Any) -> httpx.Response:
|
def request(self, method: str, path: str, **kwargs: Any) -> httpx.Response:
|
||||||
|
response: httpx.Response | None = None
|
||||||
|
for attempt in range(self.max_retries + 1):
|
||||||
response = self._client.request(method, self._build_path(path), **kwargs)
|
response = self._client.request(method, self._build_path(path), **kwargs)
|
||||||
|
if response.status_code != 429 or attempt >= self.max_retries:
|
||||||
|
return self._check_response(response)
|
||||||
|
|
||||||
|
delay = self._get_retry_delay(response, attempt + 1)
|
||||||
|
if delay > 0:
|
||||||
|
time.sleep(delay)
|
||||||
|
|
||||||
|
assert response is not None
|
||||||
return self._check_response(response)
|
return self._check_response(response)
|
||||||
|
|
||||||
def get_json(self, path: str, **kwargs: Any) -> Any:
|
def get_json(self, path: str, **kwargs: Any) -> Any:
|
||||||
|
|
@ -125,6 +150,74 @@ class EasybillClient(_BaseEasybillClient):
|
||||||
data = self.get_json("/documents", params=params)
|
data = self.get_json("/documents", params=params)
|
||||||
return self._parse_paged_result(data, Document)
|
return self._parse_paged_result(data, Document)
|
||||||
|
|
||||||
|
def get_position(self, position_id: int) -> Position:
|
||||||
|
return Position.model_validate(self.get_json(f"/positions/{position_id}"))
|
||||||
|
|
||||||
|
def list_positions(self, *, page: int = 1, limit: int | None = None, **filters: Any) -> PagedResult:
|
||||||
|
params = self._clean_params({"page": page, "limit": limit or self.default_limit, **filters})
|
||||||
|
data = self.get_json("/positions", params=params)
|
||||||
|
return self._parse_paged_result(data, Position)
|
||||||
|
|
||||||
|
def list_document_payments(self, *, page: int = 1, limit: int | None = None, **filters: Any) -> PagedResult:
|
||||||
|
params = self._clean_params({"page": page, "limit": limit or self.default_limit, **filters})
|
||||||
|
data = self.get_json("/document-payments", params=params)
|
||||||
|
return self._parse_paged_result(data, DocumentPayment)
|
||||||
|
|
||||||
|
def create_document_payment(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
document_id: int,
|
||||||
|
amount: int,
|
||||||
|
reference: str | None = None,
|
||||||
|
type: str = "CASH",
|
||||||
|
payment_at: str | None = None,
|
||||||
|
) -> DocumentPayment:
|
||||||
|
payload = {
|
||||||
|
"document_id": document_id,
|
||||||
|
"amount": amount,
|
||||||
|
"reference": reference,
|
||||||
|
"type": type,
|
||||||
|
"payment_at": payment_at,
|
||||||
|
}
|
||||||
|
data = self.post_json("/document-payments", json=self._clean_params(payload))
|
||||||
|
return DocumentPayment.model_validate(data)
|
||||||
|
|
||||||
|
def iter_all_customers(self, *, limit: int | None = None, **filters: Any):
|
||||||
|
page = 1
|
||||||
|
while True:
|
||||||
|
result = self.list_customers(page=page, limit=limit, **filters)
|
||||||
|
yield from result.items
|
||||||
|
if page >= result.pages:
|
||||||
|
break
|
||||||
|
page += 1
|
||||||
|
|
||||||
|
def iter_all_documents(self, *, limit: int | None = None, **filters: Any):
|
||||||
|
page = 1
|
||||||
|
while True:
|
||||||
|
result = self.list_documents(page=page, limit=limit, **filters)
|
||||||
|
yield from result.items
|
||||||
|
if page >= result.pages:
|
||||||
|
break
|
||||||
|
page += 1
|
||||||
|
|
||||||
|
def iter_all_positions(self, *, limit: int | None = None, **filters: Any):
|
||||||
|
page = 1
|
||||||
|
while True:
|
||||||
|
result = self.list_positions(page=page, limit=limit, **filters)
|
||||||
|
yield from result.items
|
||||||
|
if page >= result.pages:
|
||||||
|
break
|
||||||
|
page += 1
|
||||||
|
|
||||||
|
def iter_all_document_payments(self, *, limit: int | None = None, **filters: Any):
|
||||||
|
page = 1
|
||||||
|
while True:
|
||||||
|
result = self.list_document_payments(page=page, limit=limit, **filters)
|
||||||
|
yield from result.items
|
||||||
|
if page >= result.pages:
|
||||||
|
break
|
||||||
|
page += 1
|
||||||
|
|
||||||
def list_webhooks(self, *, page: int = 1, limit: int | None = None) -> PagedResult:
|
def list_webhooks(self, *, page: int = 1, limit: int | None = None) -> PagedResult:
|
||||||
params = self._clean_params({"page": page, "limit": limit or self.default_limit})
|
params = self._clean_params({"page": page, "limit": limit or self.default_limit})
|
||||||
data = self.get_json("/webhooks", params=params)
|
data = self.get_json("/webhooks", params=params)
|
||||||
|
|
@ -173,7 +266,17 @@ class AsyncEasybillClient(_BaseEasybillClient):
|
||||||
await self.aclose()
|
await self.aclose()
|
||||||
|
|
||||||
async def request(self, method: str, path: str, **kwargs: Any) -> httpx.Response:
|
async def request(self, method: str, path: str, **kwargs: Any) -> httpx.Response:
|
||||||
|
response: httpx.Response | None = None
|
||||||
|
for attempt in range(self.max_retries + 1):
|
||||||
response = await self._client.request(method, self._build_path(path), **kwargs)
|
response = await self._client.request(method, self._build_path(path), **kwargs)
|
||||||
|
if response.status_code != 429 or attempt >= self.max_retries:
|
||||||
|
return self._check_response(response)
|
||||||
|
|
||||||
|
delay = self._get_retry_delay(response, attempt + 1)
|
||||||
|
if delay > 0:
|
||||||
|
await asyncio.sleep(delay)
|
||||||
|
|
||||||
|
assert response is not None
|
||||||
return self._check_response(response)
|
return self._check_response(response)
|
||||||
|
|
||||||
async def get_json(self, path: str, **kwargs: Any) -> Any:
|
async def get_json(self, path: str, **kwargs: Any) -> Any:
|
||||||
|
|
@ -200,6 +303,78 @@ class AsyncEasybillClient(_BaseEasybillClient):
|
||||||
data = await self.get_json("/documents", params=params)
|
data = await self.get_json("/documents", params=params)
|
||||||
return self._parse_paged_result(data, Document)
|
return self._parse_paged_result(data, Document)
|
||||||
|
|
||||||
|
async def get_position(self, position_id: int) -> Position:
|
||||||
|
return Position.model_validate(await self.get_json(f"/positions/{position_id}"))
|
||||||
|
|
||||||
|
async def list_positions(self, *, page: int = 1, limit: int | None = None, **filters: Any) -> PagedResult:
|
||||||
|
params = self._clean_params({"page": page, "limit": limit or self.default_limit, **filters})
|
||||||
|
data = await self.get_json("/positions", params=params)
|
||||||
|
return self._parse_paged_result(data, Position)
|
||||||
|
|
||||||
|
async def list_document_payments(self, *, page: int = 1, limit: int | None = None, **filters: Any) -> PagedResult:
|
||||||
|
params = self._clean_params({"page": page, "limit": limit or self.default_limit, **filters})
|
||||||
|
data = await self.get_json("/document-payments", params=params)
|
||||||
|
return self._parse_paged_result(data, DocumentPayment)
|
||||||
|
|
||||||
|
async def create_document_payment(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
document_id: int,
|
||||||
|
amount: int,
|
||||||
|
reference: str | None = None,
|
||||||
|
type: str = "CASH",
|
||||||
|
payment_at: str | None = None,
|
||||||
|
) -> DocumentPayment:
|
||||||
|
payload = {
|
||||||
|
"document_id": document_id,
|
||||||
|
"amount": amount,
|
||||||
|
"reference": reference,
|
||||||
|
"type": type,
|
||||||
|
"payment_at": payment_at,
|
||||||
|
}
|
||||||
|
data = await self.post_json("/document-payments", json=self._clean_params(payload))
|
||||||
|
return DocumentPayment.model_validate(data)
|
||||||
|
|
||||||
|
async def iter_all_customers(self, *, limit: int | None = None, **filters: Any):
|
||||||
|
page = 1
|
||||||
|
while True:
|
||||||
|
result = await self.list_customers(page=page, limit=limit, **filters)
|
||||||
|
for item in result.items:
|
||||||
|
yield item
|
||||||
|
if page >= result.pages:
|
||||||
|
break
|
||||||
|
page += 1
|
||||||
|
|
||||||
|
async def iter_all_documents(self, *, limit: int | None = None, **filters: Any):
|
||||||
|
page = 1
|
||||||
|
while True:
|
||||||
|
result = await self.list_documents(page=page, limit=limit, **filters)
|
||||||
|
for item in result.items:
|
||||||
|
yield item
|
||||||
|
if page >= result.pages:
|
||||||
|
break
|
||||||
|
page += 1
|
||||||
|
|
||||||
|
async def iter_all_positions(self, *, limit: int | None = None, **filters: Any):
|
||||||
|
page = 1
|
||||||
|
while True:
|
||||||
|
result = await self.list_positions(page=page, limit=limit, **filters)
|
||||||
|
for item in result.items:
|
||||||
|
yield item
|
||||||
|
if page >= result.pages:
|
||||||
|
break
|
||||||
|
page += 1
|
||||||
|
|
||||||
|
async def iter_all_document_payments(self, *, limit: int | None = None, **filters: Any):
|
||||||
|
page = 1
|
||||||
|
while True:
|
||||||
|
result = await self.list_document_payments(page=page, limit=limit, **filters)
|
||||||
|
for item in result.items:
|
||||||
|
yield item
|
||||||
|
if page >= result.pages:
|
||||||
|
break
|
||||||
|
page += 1
|
||||||
|
|
||||||
async def list_webhooks(self, *, page: int = 1, limit: int | None = None) -> PagedResult:
|
async def list_webhooks(self, *, page: int = 1, limit: int | None = None) -> PagedResult:
|
||||||
params = self._clean_params({"page": page, "limit": limit or self.default_limit})
|
params = self._clean_params({"page": page, "limit": limit or self.default_limit})
|
||||||
data = await self.get_json("/webhooks", params=params)
|
data = await self.get_json("/webhooks", params=params)
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,25 @@ class Document(EasybillBaseModel):
|
||||||
is_draft: bool | None = None
|
is_draft: bool | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class Position(EasybillBaseModel):
|
||||||
|
id: int | None = None
|
||||||
|
number: str | None = None
|
||||||
|
type: str | None = None
|
||||||
|
description: str | None = None
|
||||||
|
price: int | float | None = None
|
||||||
|
quantity: int | float | None = None
|
||||||
|
unit: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class DocumentPayment(EasybillBaseModel):
|
||||||
|
id: int | None = None
|
||||||
|
document_id: int | None = None
|
||||||
|
amount: int | None = None
|
||||||
|
reference: str | None = None
|
||||||
|
type: str | None = None
|
||||||
|
payment_at: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class WebhookSubscription(EasybillBaseModel):
|
class WebhookSubscription(EasybillBaseModel):
|
||||||
id: int | None = None
|
id: int | None = None
|
||||||
url: str
|
url: str
|
||||||
|
|
|
||||||
155
tests/test_workflow_helpers.py
Normal file
155
tests/test_workflow_helpers.py
Normal file
|
|
@ -0,0 +1,155 @@
|
||||||
|
import httpx
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from easybill_client import AsyncEasybillClient, EasybillClient
|
||||||
|
|
||||||
|
|
||||||
|
class WorkflowTransport(httpx.MockTransport):
|
||||||
|
def __init__(self):
|
||||||
|
self.rate_limit_hits = 0
|
||||||
|
super().__init__(self._handler)
|
||||||
|
|
||||||
|
def _handler(self, request: httpx.Request) -> httpx.Response:
|
||||||
|
if request.url.path == "/positions/5":
|
||||||
|
return httpx.Response(
|
||||||
|
200,
|
||||||
|
json={
|
||||||
|
"id": 5,
|
||||||
|
"number": "ART-5",
|
||||||
|
"type": "PRODUCT",
|
||||||
|
"description": "Premium Support",
|
||||||
|
"price": 4900,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if request.url.path == "/document-payments":
|
||||||
|
if request.method == "POST":
|
||||||
|
body = request.read().decode()
|
||||||
|
assert "INV-1" in body
|
||||||
|
return httpx.Response(
|
||||||
|
201,
|
||||||
|
json={
|
||||||
|
"id": 88,
|
||||||
|
"document_id": 10,
|
||||||
|
"amount": 1999,
|
||||||
|
"reference": "INV-1",
|
||||||
|
"type": "CASH",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return httpx.Response(
|
||||||
|
200,
|
||||||
|
json={
|
||||||
|
"page": 1,
|
||||||
|
"pages": 1,
|
||||||
|
"limit": 100,
|
||||||
|
"total": 1,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": 88,
|
||||||
|
"document_id": 10,
|
||||||
|
"amount": 1999,
|
||||||
|
"reference": "INV-1",
|
||||||
|
"type": "CASH",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if request.url.path == "/customers":
|
||||||
|
page = request.url.params.get("page", "1")
|
||||||
|
if page == "1":
|
||||||
|
return httpx.Response(
|
||||||
|
200,
|
||||||
|
json={
|
||||||
|
"page": 1,
|
||||||
|
"pages": 2,
|
||||||
|
"limit": 1,
|
||||||
|
"total": 2,
|
||||||
|
"items": [{"id": 1, "company_name": "ACME GmbH"}],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return httpx.Response(
|
||||||
|
200,
|
||||||
|
json={
|
||||||
|
"page": 2,
|
||||||
|
"pages": 2,
|
||||||
|
"limit": 1,
|
||||||
|
"total": 2,
|
||||||
|
"items": [{"id": 2, "company_name": "Example AG"}],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if request.url.path == "/documents":
|
||||||
|
if self.rate_limit_hits == 0:
|
||||||
|
self.rate_limit_hits += 1
|
||||||
|
return httpx.Response(429, headers={"Retry-After": "0"}, json={"error": "slow down"})
|
||||||
|
|
||||||
|
return httpx.Response(
|
||||||
|
200,
|
||||||
|
json={
|
||||||
|
"page": 1,
|
||||||
|
"pages": 1,
|
||||||
|
"limit": 100,
|
||||||
|
"total": 1,
|
||||||
|
"items": [{"id": 10, "number": "RE-10", "amount": 1999}],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return httpx.Response(404, json={"error": "not found"})
|
||||||
|
|
||||||
|
|
||||||
|
class AsyncWorkflowTransport(httpx.MockTransport):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(self._handler)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _handler(request: httpx.Request) -> httpx.Response:
|
||||||
|
if request.url.path == "/positions":
|
||||||
|
return httpx.Response(
|
||||||
|
200,
|
||||||
|
json={
|
||||||
|
"page": 1,
|
||||||
|
"pages": 1,
|
||||||
|
"limit": 100,
|
||||||
|
"total": 1,
|
||||||
|
"items": [{"id": 7, "number": "ART-7", "description": "Hosting"}],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return httpx.Response(404, json={"error": "not found"})
|
||||||
|
|
||||||
|
|
||||||
|
def test_sync_workflow_helpers_cover_positions_payments_pagination_and_retry():
|
||||||
|
transport = WorkflowTransport()
|
||||||
|
client = EasybillClient(api_key="token", transport=transport, max_retries=1, retry_backoff=0)
|
||||||
|
try:
|
||||||
|
position = client.get_position(5)
|
||||||
|
payments = client.list_document_payments(document_id=10)
|
||||||
|
created_payment = client.create_document_payment(document_id=10, amount=1999, reference="INV-1")
|
||||||
|
all_customers = list(client.iter_all_customers(limit=1))
|
||||||
|
all_payments = list(client.iter_all_document_payments(document_id=10))
|
||||||
|
documents = client.list_documents()
|
||||||
|
finally:
|
||||||
|
client.close()
|
||||||
|
|
||||||
|
assert position.id == 5
|
||||||
|
assert position.price == 4900
|
||||||
|
assert payments.items[0].reference == "INV-1"
|
||||||
|
assert created_payment.id == 88
|
||||||
|
assert [customer.id for customer in all_customers] == [1, 2]
|
||||||
|
assert [payment.id for payment in all_payments] == [88]
|
||||||
|
assert documents.items[0].number == "RE-10"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_async_workflow_helpers_list_positions_as_models():
|
||||||
|
client = AsyncEasybillClient(api_key="token", transport=AsyncWorkflowTransport())
|
||||||
|
try:
|
||||||
|
positions = await client.list_positions()
|
||||||
|
iterated = [item async for item in client.iter_all_positions()]
|
||||||
|
finally:
|
||||||
|
await client.aclose()
|
||||||
|
|
||||||
|
assert positions.total == 1
|
||||||
|
assert positions.items[0].number == "ART-7"
|
||||||
|
assert iterated[0].id == 7
|
||||||
Loading…
Add table
Add a link
Reference in a new issue