Skip to content

Commit

Permalink
feat: return the total count of objects when using the get() endpoi…
Browse files Browse the repository at this point in the history
…nt. Also, add docs for pagination and the get endpoint changes introduced in this commit
waza-ari committed Jan 15, 2024
1 parent b93b386 commit 25d3e8e
Showing 10 changed files with 96 additions and 28 deletions.
56 changes: 53 additions & 3 deletions docs/usage.md
Original file line number Diff line number Diff line change
@@ -28,8 +28,8 @@ Once you have the client object, all supported attributes are composed into the
c = EasyvereinAPI(api_key="your_key")

# Get invoices
c.invoice.get()
c.invoice.get_all()
invoices, total_count = c.invoice.get()
invoices = c.invoice.get_all()

# Members
c.member.get()
@@ -91,7 +91,7 @@ parsing (JSON API response to models), validating (make sure all fields adhere t

To read data from the API, all CRUD endpoints feature the `get`, `get_all` and `get_by_id` endpoints.

- `get()`: Returns a single page of the resource
- `get()`: Returns a single page of the resource and the total count
- `get_all()`: Provides an abstraction layer around pagination, fetches all available resources
- `get_by_id`: Returns a single resource based on its resource id

@@ -101,6 +101,10 @@ API including auto-completion and type hinting by popular IDEs.
**Example**:

```python
# get() returns the member and total count
members, total_count = ev_connection.member.get()

# Get all returns a simple list, you can obtain the total count based on the list length
invoices = ev_connection.invoice.get_all(limit_per_page=100)

# Invoices now is a list of Invoice objects
@@ -109,6 +113,52 @@ for invoice in invoices:
print(invoice.invNumber)
```

### Using filters

The library also supports using the search parameters and filters, on the `get()` and `get_all()` endpoint. For this
to work, you need to define a filter model and pass it as parameter. The filter model is generated using the OpenAPI
specs provided by the EasyVerein API and validated as Pydantic model, too.

**Example**

```python
search = InvoiceFilter(
invNumber__in=["1", "3", "5"], canceledInvoice__isnull=True, isDraft=False
)

filtered_invoices, total_count = ev_connection.invoice.get(search=search)
```

### Pagination

The `get_all()` endpoint abstracts the need for pagination from the user, as it will simply fetch all resources of a
given endpoint, potentially by doing multiple API requests. If you need additional control, you can always fall back
to the `get()` method, which accepts two parameters:

- `limit`: elements to be returned per page
- `page`: page to return

This endpoints are passed to the query string without validation, so please make sure to stay within the limits
imposed by the EV API.

### The `get()` Endpoint and total count`

The `get()` method returns a tuple, consisting of the parsed response and the total count in addition. There`s three
possible options on how to work on that:

```python
# If you need both the returned values and the total count, simply unpack them
members, total_count = ev_connection.member.get()

# In case you don't need the total count, the preferred way is to simply access the first element of the response
members = ev_connection.member.get()[0]

# Another common option (although not recommended, because it shadows a function of the popular gettext library)
# is to use `_` as temporary variable:

members, _ = ev_connection.member.get()
```

### EasyVerein References

In many cases, a model can reference other models. For example, the `Invoice` model has a `relatedAddress` attribute. By
12 changes: 9 additions & 3 deletions easyverein/core/client.py
Original file line number Diff line number Diff line change
@@ -210,22 +210,28 @@ def upload(
status_code,
)

def fetch(self, url, model: type[T] = None) -> list[T]:
def fetch(self, url, model: type[T] = None) -> tuple[list[T], int]:
"""
Helper method that fetches a result from an API call
Only supports GET endpoints
"""
res = self._do_request("get", url)
return self._handle_response(res, model, 200)
data = self._handle_response(res, model, 200)
try:
total_count = res[1]["count"]
except KeyError:
total_count = 0

return data, total_count

def fetch_one(self, url, model: type[T] = None) -> T | None:
"""
Helper method that fetches a result from an API call
Only supports GET endpoints
"""
reply = self.fetch(url, model)
reply, _ = self.fetch(url, model)
if isinstance(reply, list):
if len(reply) == 0:
return None
4 changes: 3 additions & 1 deletion easyverein/modules/mixins/crud.py
Original file line number Diff line number Diff line change
@@ -21,11 +21,13 @@ def get(
search: FilterType = None,
limit: int = 10,
page: int = 1,
) -> list[ModelType]:
) -> tuple[list[ModelType], int]:
"""
Fetches a single page of a given page size. The page size is defined by the `limit` parameter
with an API sided upper limit of 100.
Returns a tuple, where the first element is the returned objects and the second is the total count.
Args:
query: Query to use with API. Refer to the EV API help for more information on how to use queries
search: Filter to use with API. Refer to the EV API help for more information on how to use filters
2 changes: 1 addition & 1 deletion easyverein/modules/mixins/recycle_bin.py
Original file line number Diff line number Diff line change
@@ -8,7 +8,7 @@


class RecycleBinMixin(Generic[ModelType]):
def get_deleted(self: IsEVClientProtocol) -> list[ModelType]:
def get_deleted(self: IsEVClientProtocol) -> tuple[list[ModelType], int]:
"""
Fetches all deleted resources from the recycle bin and returns a list.
"""
3 changes: 2 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
@@ -26,7 +26,8 @@ def random_string():

@pytest.fixture(scope="module")
def example_member(ev_connection):
return ev_connection.member.get()[1]
members, _ = ev_connection.member.get()
return members[0]


@pytest.fixture(scope="module")
9 changes: 5 additions & 4 deletions tests/test_contact_details.py
Original file line number Diff line number Diff line change
@@ -4,11 +4,12 @@

class TestContactDetails:
def test_get_contact_details(self, ev_connection: EasyvereinAPI):
contact_details = ev_connection.contact_details.get()
contact_details, total_count = ev_connection.contact_details.get()
# Check if the response is a list
assert isinstance(contact_details, list)

# We should have 5 invoices based on the example data
# We should have 6 invoices based on the example data
assert total_count == 6
assert len(contact_details) == 6

# Check if all the members are of type Member
@@ -27,14 +28,14 @@ def test_create_minimal_company_contact_details(self, ev_connection: EasyvereinA
ev_connection.contact_details.delete(contact_details)

# Get entries from wastebasket
deleted_contact_details = ev_connection.contact_details.get_deleted()
deleted_contact_details, _ = ev_connection.contact_details.get_deleted()
assert len(deleted_contact_details) == 1

# Finally purge contact details from wastebasket
ev_connection.contact_details.purge(contact_details.id)

# Get entries from wastebasket
deleted_contact_details = ev_connection.contact_details.get_deleted()
deleted_contact_details, _ = ev_connection.contact_details.get_deleted()
assert len(deleted_contact_details) == 0

def test_create_minimal_personal_contact_details(
5 changes: 3 additions & 2 deletions tests/test_custom_field.py
Original file line number Diff line number Diff line change
@@ -15,8 +15,9 @@ def test_create_custom_field(self, ev_connection: EasyvereinAPI):
assert custom_field.name == "Test-Field"

# Get all custom fields and check that we've got one now
custom_fields = ev_connection.custom_field.get()
custom_fields, total_count = ev_connection.custom_field.get()
assert isinstance(custom_fields, list)
assert total_count == 1
assert len(custom_fields) == 1
assert all(isinstance(f, CustomField) for f in custom_fields)

@@ -48,6 +49,6 @@ def test_create_custom_field(self, ev_connection: EasyvereinAPI):
ev_connection.custom_field.delete(custom_field)

# Now there should be none left
custom_fields = ev_connection.custom_field.get()
custom_fields = ev_connection.custom_field.get()[0]
assert isinstance(custom_fields, list)
assert len(custom_fields) == 0
13 changes: 8 additions & 5 deletions tests/test_filters.py
Original file line number Diff line number Diff line change
@@ -8,18 +8,21 @@

class TestFilter:
@staticmethod
def validate_response(response: list[Any], model: type, num_expected_results: int):
def validate_response(
response: tuple[list[Any], int], model: type, num_expected_results: int
):
"""
Helper method that validates the response of a filter
"""
# Check if the response is a list
assert isinstance(response, list)
assert isinstance(response[0], list)

# We should have 5 invoices based on the example data
assert len(response) == num_expected_results
# Assert length of response, in this case should match the total
assert len(response[0]) == num_expected_results
assert response[1] == num_expected_results

# Check if all the invoices are of type Invoice
for instance in response:
for instance in response[0]:
assert isinstance(instance, model)

def test_filter_invoices(self, ev_connection: EasyvereinAPI):
14 changes: 8 additions & 6 deletions tests/test_invoice.py
Original file line number Diff line number Diff line change
@@ -13,11 +13,12 @@

class TestInvoices:
def test_get_invoices(self, ev_connection: EasyvereinAPI):
invoices = ev_connection.invoice.get()
invoices, total_count = ev_connection.invoice.get()
# Check if the response is a list
assert isinstance(invoices, list)

# We should have 5 invoices based on the example data
assert total_count == 5
assert len(invoices) == 5

# Check if all the invoices are of type Invoice
@@ -47,7 +48,7 @@ def test_create_invoice_minimal(
ev_connection.invoice.create(invoice_model)

# Get entries from wastebasket
deleted_invoices = ev_connection.invoice.get_deleted()
deleted_invoices, _ = ev_connection.invoice.get_deleted()
assert len(deleted_invoices) == 1
assert deleted_invoices[0].id == invoice.id
assert deleted_invoices[0].invNumber == invoice.invNumber
@@ -56,7 +57,7 @@ def test_create_invoice_minimal(
ev_connection.invoice.purge(invoice.id)

# Get entries from wastebasket
deleted_invoices = ev_connection.invoice.get_deleted()
deleted_invoices, _ = ev_connection.invoice.get_deleted()
assert len(deleted_invoices) == 0

def test_create_invoice_with_items(
@@ -170,14 +171,15 @@ def test_create_invoice_with_items_helper(
# Delete invoice again
ev_connection.invoice.delete(invoice, delete_from_recycle_bin=True)
# Check that we're back to 5 invoices
invoices = ev_connection.invoice.get()
invoices, total_count = ev_connection.invoice.get()
assert total_count == 5
assert len(invoices) == 5

def test_create_invoice_with_attachment(
self, ev_connection: EasyvereinAPI, random_string: str, request: FixtureRequest
):
# Get members
members = ev_connection.member.get()
members, _ = ev_connection.member.get()
assert len(members) > 0
member = members[1]

@@ -213,5 +215,5 @@ def test_create_invoice_with_attachment(
# Delete invoice again
ev_connection.invoice.delete(invoice)
# Check that we're back to 5 invoices
invoices = ev_connection.invoice.get()
invoices, _ = ev_connection.invoice.get()
assert len(invoices) == 5
6 changes: 4 additions & 2 deletions tests/test_member.py
Original file line number Diff line number Diff line change
@@ -7,11 +7,12 @@

class TestMember:
def test_get_members(self, ev_connection: EasyvereinAPI):
members = ev_connection.member.get()
members, total_count = ev_connection.member.get()
# Check if the response is a list
assert isinstance(members, list)

# We should have 5 invoices based on the example data
assert total_count == 5
assert len(members) == 5

# Check if all the members are of type Member
@@ -24,8 +25,9 @@ def test_members_with_query(self, ev_connection: EasyvereinAPI):
"resignationDate,_isApplication}"
)

members = ev_connection.member.get(query=query, limit=2)
members, total_count = ev_connection.member.get(query=query, limit=2)
assert len(members) == 2
assert total_count == 5

for member in members:
assert isinstance(member, Member)

0 comments on commit 25d3e8e

Please sign in to comment.