From 9a65d50d3a45b32cc1581b39347aa3cc79b203d8 Mon Sep 17 00:00:00 2001 From: Daniel Herrmann Date: Thu, 11 Jan 2024 21:40:43 +0100 Subject: [PATCH] feat: add basic support for URL filters --- easyverein/models/contact_details.py | 10 +++- easyverein/models/custom_field.py | 10 +++- easyverein/models/invoice.py | 58 ++++++++++++++++++++++- easyverein/models/invoice_item.py | 10 +++- easyverein/models/member.py | 10 +++- easyverein/models/member_custom_field.py | 10 ++++ easyverein/modules/contact_details.py | 5 +- easyverein/modules/custom_field.py | 9 +++- easyverein/modules/invoice.py | 5 +- easyverein/modules/invoice_item.py | 11 ++++- easyverein/modules/member.py | 4 +- easyverein/modules/member_custom_field.py | 8 +++- easyverein/modules/mixins/crud.py | 29 +++++++++--- tests/test_filters.py | 21 ++++++++ 14 files changed, 178 insertions(+), 22 deletions(-) create mode 100644 tests/test_filters.py diff --git a/easyverein/models/contact_details.py b/easyverein/models/contact_details.py index 4055e8d..272b2da 100644 --- a/easyverein/models/contact_details.py +++ b/easyverein/models/contact_details.py @@ -5,7 +5,7 @@ from typing import Any, Literal -from pydantic import EmailStr, Field +from pydantic import BaseModel, EmailStr, Field from ..core.types import Date, DateTime from .base import EasyVereinBase @@ -135,3 +135,11 @@ class ContactDetailsCreate(ContactDetailsUpdate, required_mixin(["isCompany"])): """ Pydantic model for creating new contact details """ + + +class ContactDetailsFilter(BaseModel): + """ + Pydantic model used to filter contact details + """ + + # TODO: implement diff --git a/easyverein/models/custom_field.py b/easyverein/models/custom_field.py index 3800351..14aa7c8 100644 --- a/easyverein/models/custom_field.py +++ b/easyverein/models/custom_field.py @@ -5,7 +5,7 @@ from typing import Literal -from pydantic import Field +from pydantic import BaseModel, Field from ..core.types import ( EasyVereinReference, @@ -91,3 +91,11 @@ class CustomFieldUpdate(CustomField): """ pass + + +class CustomFieldFilter(BaseModel): + """ + Pydantic model used to filter custom fields + """ + + # TODO: implement diff --git a/easyverein/models/invoice.py b/easyverein/models/invoice.py index 8e80284..f48790f 100644 --- a/easyverein/models/invoice.py +++ b/easyverein/models/invoice.py @@ -5,7 +5,15 @@ from typing import Literal -from ..core.types import Date, EasyVereinReference, PositiveIntWithZero +from pydantic import BaseModel + +from ..core.types import ( + Date, + EasyVereinReference, + FilterIntList, + FilterStrList, + PositiveIntWithZero, +) from .base import EasyVereinBase from .mixins.required_attributes import required_mixin @@ -73,5 +81,53 @@ class InvoiceUpdate(Invoice): """ +class InvoiceFilter(BaseModel): + """ + Pydantic model used to filter invoices + """ + + id__in: FilterIntList | None = None + relatedAddress: int | None = None + relatedAddress__isnull: bool = None + relatedBookings: FilterIntList | None = None + relatedBookings__isnull: bool = None + relatedBookings__ne: FilterIntList | None = None + payedFromUser: int | None = None + payedFromUser__isnull: bool = None + approvedFromAdmin: int | None = None + approvedFromAdmin__isnull: bool = None + canceledInvoice__isnull: bool = None + date: Date | None = None + date__gt: Date | None = None + date__lt: Date | None = None + dateItHappened: Date | None = None + dateItHappened__gt: Date | None = None + dateItHappened__lt: Date | None = None + invNumber__in: FilterStrList | None = None + receiver: str | None = None + totalPrice: float | None = None + totalPrice__gte: float | None = None + totalPrice__lte: float | None = None + kind: str | None = None + kind__in: FilterStrList | None = None + refNumber: str | None = None + paymentDifference: float | None = None + paymentDifference__gte: float | None = None + paymentDifference__lte: float | None = None + paymentDifference__ne: float | None = None + isRequest: bool | None = None + accnumber: int | None = None + accnumber__ne: int | None = None + isDraft: bool | None = None + isTemplate: bool | None = None + actualCallStateName: str | None = None + actualCallStateName__ne: str | None = None + deleted: bool | None = None + customfilter: int | None = None + usesessionfilter: bool | None = None + ordering: str | None = None + search: str | None = None + + from .invoice_item import InvoiceItem # noqa: E402 from .member import Member # noqa: E402 diff --git a/easyverein/models/invoice_item.py b/easyverein/models/invoice_item.py index efe60cb..144a7f5 100644 --- a/easyverein/models/invoice_item.py +++ b/easyverein/models/invoice_item.py @@ -5,7 +5,7 @@ from typing import Annotated -from pydantic import PositiveInt, StringConstraints +from pydantic import BaseModel, PositiveInt, StringConstraints from ..core.types import EasyVereinReference from .base import EasyVereinBase @@ -53,4 +53,12 @@ class InvoiceItemUpdate(InvoiceItem): """ +class InvoiceItemFilter(BaseModel): + """ + Pydantic model used to filter invoice items + """ + + # TODO: implement + + from .invoice import Invoice # noqa: E402 diff --git a/easyverein/models/member.py b/easyverein/models/member.py index 15a0acd..3c7c42d 100644 --- a/easyverein/models/member.py +++ b/easyverein/models/member.py @@ -5,7 +5,7 @@ from typing import Literal -from pydantic import Field, PositiveInt +from pydantic import BaseModel, Field, PositiveInt from ..core.types import AnyHttpURL, DateTime, EasyVereinReference from .base import EasyVereinBase @@ -120,5 +120,13 @@ class MemberCreate(MemberUpdate, required_mixin(["contactDetails"])): emailOrUserName: str +class MemberFilter(BaseModel): + """ + Pydantic model used to filter members + """ + + # TODO: implement + + from .contact_details import ContactDetails # noqa: E402 from .member_custom_field import MemberCustomField # noqa: E402 diff --git a/easyverein/models/member_custom_field.py b/easyverein/models/member_custom_field.py index d30ead6..5abe301 100644 --- a/easyverein/models/member_custom_field.py +++ b/easyverein/models/member_custom_field.py @@ -1,5 +1,7 @@ from __future__ import annotations +from pydantic import BaseModel + from ..core.types import EasyVereinReference from .base import EasyVereinBase from .mixins.required_attributes import required_mixin @@ -47,5 +49,13 @@ class MemberCustomFieldUpdate(MemberCustomField): pass +class MemberCustomFieldFilter(BaseModel): + """ + Pydantic model used to filter members custom fields + """ + + # TODO: implement + + from .custom_field import CustomField # noqa: E402 from .member import Member # noqa: E402 diff --git a/easyverein/modules/contact_details.py b/easyverein/modules/contact_details.py index e14de2b..ebaa30b 100644 --- a/easyverein/modules/contact_details.py +++ b/easyverein/modules/contact_details.py @@ -5,12 +5,15 @@ from ..core.client import EasyvereinClient from ..models import ContactDetails, ContactDetailsCreate, ContactDetailsUpdate +from ..models.contact_details import ContactDetailsFilter from .mixins.crud import CRUDMixin from .mixins.recycle_bin import RecycleBinMixin class ContactDetailsMixin( - CRUDMixin[ContactDetails, ContactDetailsCreate, ContactDetailsUpdate], + CRUDMixin[ + ContactDetails, ContactDetailsCreate, ContactDetailsUpdate, ContactDetailsFilter + ], RecycleBinMixin[ContactDetails], ): def __init__(self, client: EasyvereinClient, logger: logging.Logger): diff --git a/easyverein/modules/custom_field.py b/easyverein/modules/custom_field.py index 78e6bd6..65f95ec 100644 --- a/easyverein/modules/custom_field.py +++ b/easyverein/modules/custom_field.py @@ -4,13 +4,18 @@ import logging from ..core.client import EasyvereinClient -from ..models.custom_field import CustomField, CustomFieldCreate, CustomFieldUpdate +from ..models.custom_field import ( + CustomField, + CustomFieldCreate, + CustomFieldFilter, + CustomFieldUpdate, +) from .mixins.crud import CRUDMixin from .mixins.recycle_bin import RecycleBinMixin class CustomFieldMixin( - CRUDMixin[CustomField, CustomFieldCreate, CustomFieldUpdate], + CRUDMixin[CustomField, CustomFieldCreate, CustomFieldUpdate, CustomFieldFilter], RecycleBinMixin[CustomField], ): def __init__(self, client: EasyvereinClient, logger: logging.Logger): diff --git a/easyverein/modules/invoice.py b/easyverein/modules/invoice.py index 57f4255..dc9a22e 100644 --- a/easyverein/modules/invoice.py +++ b/easyverein/modules/invoice.py @@ -4,14 +4,15 @@ from ..core.client import EasyvereinClient from ..core.exceptions import EasyvereinAPIException -from ..models.invoice import Invoice, InvoiceCreate, InvoiceUpdate +from ..models.invoice import Invoice, InvoiceCreate, InvoiceFilter, InvoiceUpdate from ..models.invoice_item import InvoiceItem from .mixins.crud import CRUDMixin from .mixins.recycle_bin import RecycleBinMixin class InvoiceMixin( - CRUDMixin[Invoice, InvoiceCreate, InvoiceUpdate], RecycleBinMixin[Invoice] + CRUDMixin[Invoice, InvoiceCreate, InvoiceUpdate, InvoiceFilter], + RecycleBinMixin[Invoice], ): def __init__(self, client: EasyvereinClient, logger: logging.Logger): super().__init__() diff --git a/easyverein/modules/invoice_item.py b/easyverein/modules/invoice_item.py index 52c14fb..5aade84 100644 --- a/easyverein/modules/invoice_item.py +++ b/easyverein/modules/invoice_item.py @@ -4,11 +4,18 @@ import logging from ..core.client import EasyvereinClient -from ..models.invoice_item import InvoiceItem, InvoiceItemCreate, InvoiceItemUpdate +from ..models.invoice_item import ( + InvoiceItem, + InvoiceItemCreate, + InvoiceItemFilter, + InvoiceItemUpdate, +) from .mixins.crud import CRUDMixin -class InvoiceItemMixin(CRUDMixin[InvoiceItem, InvoiceItemCreate, InvoiceItemUpdate]): +class InvoiceItemMixin( + CRUDMixin[InvoiceItem, InvoiceItemCreate, InvoiceItemUpdate, InvoiceItemFilter] +): def __init__(self, client: EasyvereinClient, logger: logging.Logger): self.endpoint_name = "invoice-item" self.return_type = InvoiceItem diff --git a/easyverein/modules/member.py b/easyverein/modules/member.py index 922e4e4..6104250 100644 --- a/easyverein/modules/member.py +++ b/easyverein/modules/member.py @@ -4,13 +4,13 @@ import logging from ..core.client import EasyvereinClient -from ..models.member import Member, MemberCreate, MemberUpdate +from ..models.member import Member, MemberCreate, MemberFilter, MemberUpdate from .mixins.crud import CRUDMixin from .mixins.recycle_bin import RecycleBinMixin class MemberMixin( - CRUDMixin[Member, MemberCreate, MemberUpdate], RecycleBinMixin[Member] + CRUDMixin[Member, MemberCreate, MemberUpdate, MemberFilter], RecycleBinMixin[Member] ): def __init__(self, client: EasyvereinClient, logger: logging.Logger): self.endpoint_name = "member" diff --git a/easyverein/modules/member_custom_field.py b/easyverein/modules/member_custom_field.py index accec83..3ea6f40 100644 --- a/easyverein/modules/member_custom_field.py +++ b/easyverein/modules/member_custom_field.py @@ -6,12 +6,18 @@ from ..core.client import EasyvereinClient from ..core.protocol import IsEVClientProtocol from ..models import MemberCustomField, MemberCustomFieldCreate, MemberCustomFieldUpdate +from ..models.member_custom_field import MemberCustomFieldFilter from .mixins.crud import CRUDMixin from .mixins.recycle_bin import RecycleBinMixin class MemberCustomFieldMixin( - CRUDMixin[MemberCustomField, MemberCustomFieldCreate, MemberCustomFieldUpdate], + CRUDMixin[ + MemberCustomField, + MemberCustomFieldCreate, + MemberCustomFieldUpdate, + MemberCustomFieldFilter, + ], RecycleBinMixin[MemberCustomField], ): def __init__(self, client: EasyvereinClient, logger: logging.Logger): diff --git a/easyverein/modules/mixins/crud.py b/easyverein/modules/mixins/crud.py index 30f9a10..6f148b9 100644 --- a/easyverein/modules/mixins/crud.py +++ b/easyverein/modules/mixins/crud.py @@ -11,11 +11,15 @@ ModelType = TypeVar("ModelType", bound=BaseModel) CreateModelType = TypeVar("CreateModelType", bound=BaseModel) UpdateModelType = TypeVar("UpdateModelType", bound=BaseModel) +FilterType = TypeVar("FilterType", bound=BaseModel) -class CRUDMixin(Generic[ModelType, CreateModelType, UpdateModelType]): +class CRUDMixin(Generic[ModelType, CreateModelType, UpdateModelType, FilterType]): def get( - self: IsEVClientProtocol, query: str = None, limit: int = 10 + self: IsEVClientProtocol, + query: str = None, + search: FilterType = None, + limit: int = 10, ) -> list[ModelType]: """ Fetches a single page of a given page size. The page size is defined by the `limit` parameter @@ -23,16 +27,24 @@ def get( 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 limit: Defines how many resources to return. """ self.logger.info(f"Fetching selected {self.endpoint_name} objects from API") - url = self.c.get_url(f"/{self.endpoint_name}", {"limit": limit, "query": query}) + url_params = {"limit": limit, "query": query} + if search: + url_params |= search.model_dump(exclude_unset=True, exclude_defaults=True) + + url = self.c.get_url(f"/{self.endpoint_name}", url_params) return self.c.fetch(url, self.return_type) def get_all( - self: IsEVClientProtocol, query: str = None, limit_per_page: int = 10 + self: IsEVClientProtocol, + query: str = None, + search: FilterType = None, + limit_per_page: int = 10, ) -> list[ModelType]: """ Convenient method that fetches all objects from the EV API, abstracting away the need to handle pagination. @@ -43,13 +55,16 @@ def get_all( Args: query: Query to use with API. Defaults to None. 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 limit_per_page: Defines how many resources to return. Defaults to 10. """ self.logger.info(f"Fetching selected {self.endpoint_name} objects from API") - url = self.c.get_url( - f"/{self.endpoint_name}", {"limit": limit_per_page, "query": query} - ) + url_params = {"limit": limit_per_page, "query": query} + if search: + url_params |= search.model_dump(exclude_unset=True, exclude_defaults=True) + + url = self.c.get_url(f"/{self.endpoint_name}", url_params) return self.c.fetch_paginated(url, self.return_type, limit_per_page) diff --git a/tests/test_filters.py b/tests/test_filters.py new file mode 100644 index 0000000..e805c1b --- /dev/null +++ b/tests/test_filters.py @@ -0,0 +1,21 @@ +from easyverein import EasyvereinAPI +from easyverein.models.invoice import Invoice, InvoiceFilter + + +class TestFilter: + def test_filter_invoices(self, ev_connection: EasyvereinAPI): + search = InvoiceFilter( + invNumber__in=["1", "3", "5"], canceledInvoice__isnull=True, isDraft=False + ) + + invoices = ev_connection.invoice.get(search=search) + + # Check if the response is a list + assert isinstance(invoices, list) + + # We should have 5 invoices based on the example data + assert len(invoices) == 3 + + # Check if all the invoices are of type Invoice + for invoice in invoices: + assert isinstance(invoice, Invoice)