Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/api/endpoints/billing_account.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ None

## Generic Model Mappings

| Generic Model | Invoice Model |
| Generic Model | BillingAccount Model |
|-------------------|------------------------|
| `ModelType` | `BillingAccount` |
| `CreateModelType` | `BillingAccountCreate` |
Expand Down
4 changes: 2 additions & 2 deletions docs/api/endpoints/booking.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ None

## Generic Model Mappings

| Generic Model | Invoice Model |
|-------------------|------------------------|
| Generic Model | Booking Model |
|-------------------|-----------------|
| `ModelType` | `Booking` |
| `CreateModelType` | `BookingCreate` |
| `UpdateModelType` | `BookingUpdate` |
Expand Down
2 changes: 1 addition & 1 deletion docs/api/endpoints/contact_details.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ None

## Generic Model Mappings

| Generic Model | Invoice Model |
| Generic Model | ContactDetails Model |
|-------------------|------------------------|
| `ModelType` | `ContactDetails` |
| `CreateModelType` | `ContactDetailsCreate` |
Expand Down
2 changes: 1 addition & 1 deletion docs/api/endpoints/custom_field.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ None

## Generic Model Mappings

| Generic Model | Invoice Model |
| Generic Model | CustomField Model |
|-------------------|---------------------|
| `ModelType` | `CustomField` |
| `CreateModelType` | `CustomFieldCreate` |
Expand Down
2 changes: 1 addition & 1 deletion docs/api/endpoints/invoice_item.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ None

## Generic Model Mappings

| Generic Model | Invoice Model |
| Generic Model | InvoiceItem Model |
|-------------------|---------------------|
| `ModelType` | `InvoiceItem` |
| `CreateModelType` | `InvoiceItemCreate` |
Expand Down
2 changes: 1 addition & 1 deletion docs/api/endpoints/member.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ None

## Generic Model Mappings

| Generic Model | Invoice Model |
| Generic Model | Member Model |
|-------------------|----------------|
| `ModelType` | `Member` |
| `CreateModelType` | `MemberCreate` |
Expand Down
2 changes: 1 addition & 1 deletion docs/api/endpoints/member_custom_field.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ requires the member as constructor argument, to avoid passing it again to all en

## Generic Model Mappings

| Generic Model | Invoice Model |
| Generic Model | MemberCustomField Model |
|-------------------|---------------------------|
| `ModelType` | `MemberCustomField` |
| `CreateModelType` | `MemberCustomFieldCreate` |
Expand Down
6 changes: 3 additions & 3 deletions docs/api/endpoints/member_group.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ None

## Generic Model Mappings

| Generic Model | Invoice Model |
|-------------------|----------------|
| `ModelType` | `MemberGroup` |
| Generic Model | Invoice Model |
|-------------------|---------------------|
| `ModelType` | `MemberGroup` |
| `CreateModelType` | `MemberGroupCreate` |
| `UpdateModelType` | `MemberGroupUpdate` |

Expand Down
2 changes: 1 addition & 1 deletion docs/api/endpoints/member_member_group.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ requires the member as constructor argument, to avoid passing it again to all en

## Generic Model Mappings

| Generic Model | Invoice Model |
| Generic Model | MemberMemberGroup Model |
|-------------------|---------------------------|
| `ModelType` | `MemberMemberGroup` |
| `CreateModelType` | `MemberMemberGroupCreate` |
Expand Down
32 changes: 32 additions & 0 deletions easyverein/core/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,38 @@ def update(self, url, data: BaseModel, status_code: int = 200, exclude_none: boo
expected_status_code=status_code,
)

def bulk_create(self, url, data: list[BaseModel], status_code: int = 201) -> ResponseSchema:
"""
Method to create multiple objects in the API
"""
return self._handle_response(
self._do_request(
"post",
url,
data={"entries": [d.model_dump(exclude_none=True, exclude_unset=True, by_alias=True) for d in data]},
),
expected_status_code=status_code,
)

def bulk_update(
self, url, data: list[BaseModel], status_code: int = 200, exclude_none: bool = True
) -> ResponseSchema:
"""
Method to update multiple objects in the API
"""
return self._handle_response(
self._do_request(
"patch",
url,
data={
"entries": [
d.model_dump(exclude_none=exclude_none, exclude_unset=True, by_alias=True) for d in data
]
},
),
expected_status_code=status_code,
)

def upload(
self,
url: str,
Expand Down
2 changes: 1 addition & 1 deletion easyverein/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
InvoiceItemFilter,
InvoiceItemUpdate,
)
from .member import Member, MemberCreate, MemberFilter, MemberUpdate
from .member import Member, MemberCreate, MemberFilter, MemberSetDosb, MemberSetLsb, MemberUpdate
from .member_custom_field import (
MemberCustomField,
MemberCustomFieldCreate,
Expand Down
11 changes: 11 additions & 0 deletions easyverein/models/base.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from pydantic import BaseModel, Field, PositiveInt

from ..core.types import DateTime, EasyVereinReference
from .mixins.empty_strings_mixin import EmptyStringsToNone


class EasyVereinBase(BaseModel):
Expand All @@ -15,3 +16,13 @@ class EasyVereinBase(BaseModel):
"""Alias for `_deleteAfterDate` field. See [Pydantic Models](../usage.md#pydantic-models) for details."""
deletedBy: str | None = Field(default=None, alias="_deletedBy")
"""Alias for `_deletedBy` field. See [Pydantic Models](../usage.md#pydantic-models) for details."""


class LsbDosbSport(EasyVereinBase, EmptyStringsToNone):
"""
Pydantic Model for a LSB/DOSB Sport.
"""

title: str | None = None
sportNumber: str | None = None
federationNumber: str | None = None
30 changes: 26 additions & 4 deletions easyverein/models/member.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
FilterIntList,
FilterStrList,
)
from .base import EasyVereinBase
from .base import EasyVereinBase, LsbDosbSport
from .mixins.empty_strings_mixin import EmptyStringsToNone
from .mixins.required_attributes import required_mixin

Expand Down Expand Up @@ -63,7 +63,6 @@ class MemberBase(EasyVereinBase):
paymentIntervallMonths: PositiveInt | Literal[-1] = 1
useBalanceForMembershipFee: bool | None = None
bulletinBoardNewPostNotification: bool | None = None
integrationDosbGender: Literal["m", "w", "d"] | None = None
isApplication: bool | None = Field(default=None, alias="_isApplication")
"""
Alias for `_isApplication` field. See [Pydantic Models](../usage.md#pydantic-models) for details.
Expand All @@ -83,8 +82,10 @@ class MemberBase(EasyVereinBase):
Alias for `_editableByRelatedMembers` field. See [Pydantic Models](../usage.md#pydantic-models) for details.
"""
sepaMandateFile: AnyHttpURL | str | None = None
# TODO: exact type is not specified in API docs
integrationDosbSport: list | None = None
integrationLsbSport: list[EasyVereinReference] | list[LsbDosbSport] | None = None
integrationLsbGender: Literal["m", "w", "d"] | None = None
integrationDosbSport: list[EasyVereinReference] | list[LsbDosbSport] | None = None
integrationDosbGender: Literal["m", "w", "d"] | None = None
customFields: list[EasyVereinReference] | list[MemberCustomField] | None = None
memberGroups: list[EasyVereinReference] | list[MemberMemberGroup] | None = None

Expand Down Expand Up @@ -189,6 +190,27 @@ class MemberFilter(BaseModel):
search: str | None = None


class MemberSetLsb(BaseModel):
"""
Pydantic model used to set LSB sports for a member

Does not match the documentation, but works (with v2.0 in january 2026 at least)
"""

lsbSport: list[str]


class MemberSetDosb(BaseModel):
"""
Pydantic model used to set DOSB sports for a member

Does not match the documentation, but works (with v2.0 in january 2026 at least)
And for some reason, has another format than the set-lsb endpoint
"""

dosb_sport: list[str]


from .contact_details import ContactDetails # noqa: E402
from .member_custom_field import MemberCustomField # noqa: E402
from .member_member_group import MemberMemberGroup # noqa: E402
3 changes: 2 additions & 1 deletion easyverein/modules/booking.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@
BookingFilter,
BookingUpdate,
)
from .mixins.crud import CRUDMixin
from .mixins.crud import BulkUpdateCreateMixin, CRUDMixin
from .mixins.recycle_bin import RecycleBinMixin


class BookingMixin(
CRUDMixin[Booking, BookingCreate, BookingUpdate, BookingFilter],
BulkUpdateCreateMixin[Booking, BookingCreate, BookingUpdate],
RecycleBinMixin[Booking],
):
def __init__(self, client: EasyvereinClient, logger: logging.Logger):
Expand Down
3 changes: 2 additions & 1 deletion easyverein/modules/contact_details.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@
from ..core.client import EasyvereinClient
from ..models import ContactDetails, ContactDetailsCreate, ContactDetailsUpdate
from ..models.contact_details import ContactDetailsFilter
from .mixins.crud import CRUDMixin
from .mixins.crud import BulkUpdateCreateMixin, CRUDMixin
from .mixins.recycle_bin import RecycleBinMixin


class ContactDetailsMixin(
CRUDMixin[ContactDetails, ContactDetailsCreate, ContactDetailsUpdate, ContactDetailsFilter],
BulkUpdateCreateMixin[ContactDetails, ContactDetailsCreate, ContactDetailsUpdate],
RecycleBinMixin[ContactDetails],
):
def __init__(self, client: EasyvereinClient, logger: logging.Logger):
Expand Down
3 changes: 2 additions & 1 deletion easyverein/modules/invoice.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@
from ..core.exceptions import EasyvereinAPIException
from ..models.invoice import Invoice, InvoiceCreate, InvoiceFilter, InvoiceUpdate
from ..models.invoice_item import InvoiceItemCreate
from .mixins.crud import CRUDMixin
from .mixins.crud import BulkUpdateCreateMixin, CRUDMixin
from .mixins.recycle_bin import RecycleBinMixin


class InvoiceMixin(
CRUDMixin[Invoice, InvoiceCreate, InvoiceUpdate, InvoiceFilter],
BulkUpdateCreateMixin[Invoice, InvoiceCreate, InvoiceUpdate],
RecycleBinMixin[Invoice],
):
def __init__(self, client: EasyvereinClient, logger: logging.Logger):
Expand Down
28 changes: 25 additions & 3 deletions easyverein/modules/member.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,18 @@
from easyverein.modules.member_member_group import MemberMemberGroupMixin

from ..core.client import EasyvereinClient
from ..models import Member, MemberCreate, MemberFilter, MemberUpdate
from .mixins.crud import CRUDMixin
from ..models import Member, MemberCreate, MemberFilter, MemberSetLsb, MemberUpdate
from ..models.member import MemberSetDosb
from .mixins.crud import BulkUpdateCreateMixin, CRUDMixin
from .mixins.helper import get_id
from .mixins.recycle_bin import RecycleBinMixin


class MemberMixin(CRUDMixin[Member, MemberCreate, MemberUpdate, MemberFilter], RecycleBinMixin[Member]):
class MemberMixin(
CRUDMixin[Member, MemberCreate, MemberUpdate, MemberFilter],
BulkUpdateCreateMixin[Member, MemberCreate, MemberUpdate],
RecycleBinMixin[Member],
):
def __init__(self, client: EasyvereinClient, logger: logging.Logger):
self.endpoint_name = "member"
self.c = client
Expand All @@ -25,3 +31,19 @@ def custom_field(self, member_id: int) -> MemberCustomFieldMixin:

def member_group(self, member_id: int) -> MemberMemberGroupMixin:
return MemberMemberGroupMixin(self.c, self.logger, member_id)

def set_lsb(self, target: Member | int, data: MemberSetLsb) -> None:
obj_id = get_id(target)

self.logger.info(f"Setting LSB sports for member {obj_id}")

url = self.c.get_url(f"/{self.endpoint_name}/{obj_id}/set-lsb")
self.c.update(url, data, status_code=204)

def set_dosb(self, target: Member | int, data: MemberSetDosb) -> None:
obj_id = get_id(target)

self.logger.info(f"Setting DOSB sports for member {obj_id}")

url = self.c.get_url(f"/{self.endpoint_name}/{obj_id}/set-dosb")
self.c.update(url, data, status_code=204)
66 changes: 66 additions & 0 deletions easyverein/modules/mixins/crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,3 +194,69 @@ def delete(
self.logger.info(f"Deleting object of type {self.endpoint_name} with id {obj_id} from wastebasket")
purge: Callable = getattr(self, "purge")
purge(obj_id)


class BulkUpdateCreateMixin(Generic[ModelType, CreateModelType, UpdateModelType]):
"""
Mixin providing bulk create and update functionality for endpoints that support it.
Currently only supported for the `member` and `contact-details` endpoints.
"""

def bulk_create(self: EVClientProtocol[ModelType], data: list[CreateModelType]) -> list[bool]:
"""
Creates multiple objects in a single API request and returns the created objects.

**Example**:

```py
from easyverein import EasyvereinAPI

ev_client = EasyvereinAPI("your_api_key")

contact_details = ev_client.contact_details.bulk_create([
ContactDetailsCreate(fistName="example1", lastName="Example1", isCompany=False),
ContactDetailsCreate(fistName="example2", lastName="Example2", isCompany=False),
])
```

Args:
data: List of Pydantic models containing the data for the objects to be created.
"""
self.logger.info(f"Creating object of type {self.endpoint_name}")

url = self.c.get_url(f"/{self.endpoint_name}/bulk-create")
response = self.c.bulk_create(url, data)
return [r["data"]["success"] for r in response.result] # type: ignore

def bulk_update(
self: EVClientProtocol[ModelType],
data: list[UpdateModelType],
exclude_none: bool = True,
) -> list[bool]:
"""
Updates multiple objects in a single API request and returns the updated objects.

Note that the update models must include the `id` of the objects to be updated.

**Example**:

```py
from easyverein import EasyvereinAPI

ev_client = EasyvereinAPI("your_api_key")

updated_members = ev_client.member.bulk_update([
MemberUpdate(id=1, membershipNumber="M1"),
MemberUpdate(id=2, membershipNumber="M2"),
])
```

Args:
data: List of Pydantic models containing the data to update.
exclude_none: If True, fields with None values will be excluded from the update.
"""
self.logger.info(f"Bulk updating objects of type {self.endpoint_name}")

url = self.c.get_url(f"/{self.endpoint_name}/bulk-update")
response = self.c.bulk_update(url, data, exclude_none=exclude_none)
return [r["data"]["success"] for r in response.result] # type: ignore
4 changes: 2 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,15 @@ def ev_connection():

@pytest.fixture(scope="module", autouse=True)
def _clear_wastebaskets(ev_connection: EasyvereinAPI):
for module in ["member_group", "custom_field", "contact_details", "billing_account"]:
for module in ["member", "member_group", "custom_field", "contact_details", "billing_account"]:
deleted, _ = getattr(ev_connection, module).get_deleted()
for d in deleted:
getattr(ev_connection, module).purge(d)


@pytest.fixture(scope="function")
def random_string():
return "".join(random.choice(string.ascii_uppercase + string.digits) for _ in range(16))
return "test_" + "".join(random.choice(string.ascii_uppercase + string.digits) for _ in range(16))


@pytest.fixture(scope="module")
Expand Down
Loading
Loading