Skip to content
Open
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
4 changes: 1 addition & 3 deletions backend/tests/api/routes/test_life_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,9 +229,7 @@ def test_delete_life_event_not_found(
assert r.status_code == 404


def test_life_event_isolation_between_users(
client: TestClient, db: Session
) -> None:
def test_life_event_isolation_between_users(client: TestClient, db: Session) -> None:
from tests.utils.user import (
authentication_token_from_email,
create_random_user,
Expand Down
4 changes: 1 addition & 3 deletions backend/tests/api/routes/test_pets.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,7 @@ def test_create_pet_unknown_contact_404(
assert r.status_code == 404


def test_list_pets(
client: TestClient, superuser_token_headers: dict[str, str]
) -> None:
def test_list_pets(client: TestClient, superuser_token_headers: dict[str, str]) -> None:
contact_id = _create_contact(client, superuser_token_headers)
client.post(
f"{settings.API_V1_STR}/pets/",
Expand Down
131 changes: 131 additions & 0 deletions kind
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
"""Reminders resource for Kindred SDK."""

from personal_crm_client import AuthenticatedClient, Client
from personal_crm_client.models import (
ReminderCreate,
ReminderUpdate,
ReminderPublic,
RemindersPublic,
HTTPValidationError,
)
from uuid import UUID

from typing import Optional


class RemindersResource:
"""Resource for managing reminders."""

def __init__(self, client: AuthenticatedClient | Client) -> None:
self._client = client

def list(
self,
*,
skip: int = 0,
limit: int = 100,
contact_id: Optional[UUID] = None,
) -> RemindersPublic | HTTPValidationError | None:
"""List reminders."""
from personal_crm_client.api.reminders.reminders_list_reminders import sync

return sync(
client=self._client,
skip=skip,
limit=limit,
contact_id=contact_id,
)

async def list_async(
self,
*,
skip: int = 0,
limit: int = 100,
contact_id: Optional[UUID] = None,
) -> RemindersPublic | HTTPValidationError | None:
"""Async version of list()."""
from personal_crm_client.api.reminders.reminders_list_reminders import asyncio

return await asyncio(
client=self._client,
skip=skip,
limit=limit,
contact_id=contact_id,
)

def get(self, reminder_id: UUID) -> ReminderPublic | HTTPValidationError | None:
"""Get a single reminder by ID."""
from personal_crm_client.api.reminders.reminders_list_reminders import sync

reminders = self.list()
if reminders and hasattr(reminders, 'data'):
for reminder in reminders.data:
if reminder.id == reminder_id:
return reminder
return None

def create(self, item: ReminderCreate) -> ReminderPublic | HTTPValidationError | None:
"""Create a new reminder."""
from personal_crm_client.api.reminders.reminders_create_reminder_route import sync

return sync(client=self._client, body=item)

async def create_async(self, item: ReminderCreate) -> ReminderPublic | HTTPValidationError | None:
"""Async version of create()."""
from personal_crm_client.api.reminders.reminders_create_reminder_route import asyncio

return await asyncio(client=self._client, body=item)

def update(
self, reminder_id: UUID, item: ReminderUpdate
) -> ReminderPublic | HTTPValidationError | None:
"""Update an existing reminder."""
from personal_crm_client.api.reminders.reminders_update_reminder import sync

return sync(client=self._client, reminder_id=reminder_id, body=item)

async def update_async(
self, reminder_id: UUID, item: ReminderUpdate
) -> ReminderPublic | HTTPValidationError | None:
"""Async version of update()."""
from personal_crm_client.api.reminders.reminders_update_reminder import asyncio

return await asyncio(client=self._client, reminder_id=reminder_id, body=item)

def delete(self, reminder_id: UUID) -> ReminderPublic | HTTPValidationError | None:
"""Delete a reminder."""
from personal_crm_client.api.reminders.reminders_delete_reminder import sync

return sync(client=self._client, reminder_id=reminder_id)

async def delete_async(
self, reminder_id: UUID
) -> ReminderPublic | HTTPValidationError | None:
"""Async version of delete()."""
from personal_crm_client.api.reminders.reminders_delete_reminder import asyncio

return await asyncio(client=self._client, reminder_id=reminder_id)

def snooze(
self, reminder_id: UUID, *, snooze_until: str
) -> ReminderPublic | HTTPValidationError | None:
"""Snooze a reminder until a specified time."""
from personal_crm_client.api.reminders.reminders_snooze_reminder import sync

return sync(
client=self._client,
reminder_id=reminder_id,
snooze_until=snooze_until,
)

async def snooze_async(
self, reminder_id: UUID, *, snooze_until: str
) -> ReminderPublic | HTTPValidationError | None:
"""Async version of snooze()."""
from personal_crm_client.api.reminders.reminders_snooze_reminder import asyncio

return await asyncio(
client=self._client,
reminder_id=reminder_id,
snooze_until=snooze_until,
)
27 changes: 27 additions & 0 deletions kindred
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
[tool.poetry]
name = "kindred-sdk"
version = "0.1.0"
description = "A typed Python client for Personal CRM"
authors = ["Will <will@example.com>"]
readme = "README.md"
packages = [
{ include = "kindred_sdk" },
{ include = "personal_crm_client" },
]
include = ["kindred_sdk/py.typed", "personal_crm_client/py.typed"]

[tool.poetry.dependencies]
python = "^3.10"
httpx = ">=0.23.0,<0.29.0"
attrs = ">=22.2.0"
python-dateutil = "^2.8.0"

[build-system]
requires = ["poetry-core>=2.0.0,<3.0.0"]
build-backend = "poetry.core.masonry.api"

[tool.ruff]
line-length = 120

[tool.ruff.lint]
select = ["F", "I", "UP"]
23 changes: 23 additions & 0 deletions kindred-sdk/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
__pycache__/
build/
dist/
*.egg-info/
.pytest_cache/

# pyenv
.python-version

# Environments
.env
.venv

# mypy
.mypy_cache/
.dmypy.json
dmypy.json

# JetBrains
.idea/

/coverage.xml
/.coverage
107 changes: 107 additions & 0 deletions kindred-sdk/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# Kindred SDK

A typed Python client for [Personal CRM](https://github.com/will/personal-crm), generated from its OpenAPI schema.

The SDK provides a clean, resource-oriented interface (`kindred_sdk`) built on top of the mechanically generated `personal_crm_client`. Use it for scripting, automations (including n8n custom nodes), and any future third-party integrations.

## Installation

```bash
pip install kindred-sdk
```

Or, if you're using Poetry:

```bash
poetry add kindred-sdk
```

## Quick Start

```python
from kindred_sdk import KindredClient

# Create a client (authenticated)
client = KindredClient(
base_url="http://localhost:8000",
token="your-api-token",
)

# List contacts
contacts = client.contacts.list()
if contacts and hasattr(contacts, 'data'):
for contact in contacts.data:
print(f"{contact.first_name} {contact.last_name}")

# Get a specific contact
from uuid import UUID
contact = client.contacts.get(contact_id=UUID("your-contact-uuid"))

# Create a new contact
from personal_crm_client.models import ContactCreate
new_contact = client.contacts.create(
ContactCreate(first_name="John", last_name="Doe")
)

# Use async
import asyncio

async def main():
async with KindredClient(
base_url="http://localhost:8000",
token="your-api-token",
) as client:
contacts = await client.contacts.list_async()
print(contacts)

asyncio.run(main())
```

## Resources

The SDK provides the following resource-oriented interfaces:

- `client.contacts` - Manage contacts (list, get, create, update, delete, restore, mentions, losing touch, household)
- `client.groups` - Manage contact groups
- `client.interactions` - Log and manage interactions
- `client.tags` - Manage tags
- `client.notes` - Manage notes with contact mentions
- `client.gifts` - Track gifts
- `client.debts` - Track debts between contacts
- `client.pets` - Manage contact pets
- `client.addresses` - Manage contact addresses
- `client.relationships` - Manage relationships between contacts
- `client.reminders` - Manage reminders (with snooze support)
- `client.life_events` - Track life events for contacts
- `client.journal` - Personal journal entries
- `client.custom_fields` - Custom field definitions and values
- `client.activity_logs` - Read-only access to activity logs
- `client.calendar` - Calendar views by month

## Regeneration

The `personal_crm_client` package is generated from the backend's OpenAPI schema using [openapi-python-client](https://github.com/openapi-generators/openapi-python-client). To regenerate:

```bash
# From the project root
docker compose exec backend uv run python -c "import json; from app.main import app; print(json.dumps(app.openapi()))" > openapi.json
cd kindred-sdk
openapi-python-client generate --url ../openapi.json --output . --overwrite
```

## Development

```bash
cd kindred-sdk
poetry install
poetry shell
```

Run linting:
```bash
ruff check .
```

## License

MIT (or your chosen license)
39 changes: 39 additions & 0 deletions kindred-sdk/kindred_sdk/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""Kindred SDK - A typed Python client for Personal CRM."""

from .client import KindredClient
from .resources.activity_logs import ActivityLogsResource
from .resources.addresses import AddressesResource
from .resources.calendar import CalendarResource
from .resources.contacts import ContactsResource
from .resources.custom_fields import CustomFieldsResource
from .resources.debts import DebtsResource
from .resources.gifts import GiftsResource
from .resources.groups import GroupsResource
from .resources.interactions import InteractionsResource
from .resources.journal import JournalResource
from .resources.life_events import LifeEventsResource
from .resources.notes import NotesResource
from .resources.pets import PetsResource
from .resources.relationships import RelationshipsResource
from .resources.reminders import RemindersResource
from .resources.tags import TagsResource

__all__ = [
"KindredClient",
"ContactsResource",
"GroupsResource",
"InteractionsResource",
"TagsResource",
"NotesResource",
"GiftsResource",
"DebtsResource",
"PetsResource",
"AddressesResource",
"RelationshipsResource",
"RemindersResource",
"LifeEventsResource",
"JournalResource",
"CustomFieldsResource",
"ActivityLogsResource",
"CalendarResource",
]
Loading
Loading