Metadata-Version: 2.4
Name: meemo
Version: 0.0.7
Summary: Meemo Python Client - Python client library for Meemo External API
Author-email: GDP Labs <jobs@gdplabs.id>
Requires-Python: <3.14,>=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: httpx>=0.28.1
Requires-Dist: pydantic>=2.9.0
Dynamic: license-file

# Meemo

A Python library for interacting with the Meemo External API, providing access to meeting data including details, transcripts, summaries, participants, and recordings.

## Prerequisites

- **Python** >=3.11, <3.14

## Installation

```bash
pip install meemo
```

Or with uv:

```bash
uv add meemo
```

## Quick Start

```python
from meemo import MeemoClient

# Initialize the client
client = MeemoClient(
    client_id="your-client-id",
    client_secret="your-client-secret",
    base_url="https://api-meemo.glair.ai",
)

# List meetings
meetings = client.meetings.list_meetings()
for meeting in meetings.results:
    print(f"{meeting.id}: {meeting.title} ({meeting.status})")

# Get meeting details
detail = client.meetings.get_meeting(123)
print(f"Host: {detail.host.name}")
print(f"Duration: {detail.duration_seconds}s")

# Get transcript
transcript = client.meetings.get_transcript(123)
for segment in transcript.transcripts:
    print(f"[{segment.speaker}] {segment.text}")

# Get summary
summary = client.meetings.get_summary(123)
print(summary.summary)

# Get summary with seekable video timestamps (for messaging UIs)
rich = client.meetings.get_summary_with_video(123)
print(rich.inline_message_markdown)  # for AIP agents; send verbatim
```

## Async Usage

For use in async frameworks (FastAPI, Django async views, async workers), use `AsyncMeemoClient`:

```python
import asyncio
from meemo import AsyncMeemoClient

async def main():
    client = AsyncMeemoClient(
        client_id="your-client-id",
        client_secret="your-client-secret",
        base_url="https://api-meemo.glair.ai",
    )

    # All methods are async — same API surface as MeemoClient
    meetings = await client.meetings.list_meetings()
    for meeting in meetings.results:
        print(f"{meeting.id}: {meeting.title}")

    detail = await client.meetings.get_meeting(123)
    transcript = await client.meetings.get_transcript(123)
    summary = await client.meetings.get_summary(123)

    # Revoke token when done
    await client.revoke_token()

asyncio.run(main())
```

## Configuration

### Environment Variables

The library supports the following environment variables:

- `MEEMO_CLIENT_ID`: OAuth2 client ID for authentication
- `MEEMO_CLIENT_SECRET`: OAuth2 client secret for authentication
- `MEEMO_BASE_URL`: Base URL of the Meemo instance

### Client Initialization

```python
from meemo import MeemoClient

# Using explicit parameters
client = MeemoClient(
    client_id="your-client-id",
    client_secret="your-client-secret",
    base_url="https://api-meemo.glair.ai",
    timeout=60.0,
    default_headers={"X-Custom-Header": "value"},
)

# Using environment variables
import os
os.environ["MEEMO_CLIENT_ID"] = "your-client-id"
os.environ["MEEMO_CLIENT_SECRET"] = "your-client-secret"
os.environ["MEEMO_BASE_URL"] = "https://api-meemo.glair.ai"
client = MeemoClient()
```

## Authentication

The library uses **OAuth2 Client Credentials** flow. Tokens are automatically obtained and refreshed when they expire (default: 3 hours).

```python
# Token management is automatic, but you can also manage it manually:

# Revoke the current token
client.revoke_token()
```

## Meetings API

### List Meetings

```python
# Get all meetings
meetings = client.meetings.list_meetings()
print(f"Total: {meetings.count}")

# Filter by organization
meetings = client.meetings.list_meetings(organization_id=5)

# Filter by title and date
meetings = client.meetings.list_meetings(
    title="standup",
    created_after="2024-01-01",
    created_before="2024-12-31",
    summary_complete=True,
)

# Pagination
meetings = client.meetings.list_meetings(page=2, size=20)

# Resolve a Google Calendar event id to a meeting (None until auto-join creates it)
meeting = client.meetings.find_meeting_by_calendar_event_id(
    "google_event_abc123",
    organization_id=5,
)
if meeting:
    print(meeting.id, meeting.title)
```

`find_meeting_by_calendar_event_id` returns a `MeetingListItem` (the list row,
not the full detail) or `None`, and verifies the returned row's
`calendar_event.event_id` before handing it back, so it answers `None` rather
than an unrelated meeting when talking to an api-service build that predates the
`calendar_event_id` filter. Call `get_meeting(meeting.id)` for full metadata.

All filters are keyword-only (see `MeetingListKwargs` for the accepted keys);
`extra_headers` remains the only positional argument.

#### Search by Summary

`search` runs a ranked lexical search over the meeting title, calendar invite
summary, the AI-generated summary markdown (the `summary` field from
`get_summary` / `summary_markdown` on `GET .../summary/` — **not** `notes`), and
tags — so you can find a meeting by what was discussed in it, not just by its
title. Results come back ordered by relevance instead of start time.

```python
# Find meetings whose summary (or title/tags) mentions the query
meetings = client.meetings.list_meetings(search="pricing model")
for m in meetings.results:
    print(f"{m.id}: {m.title}")

# Combine with any other filter (all active filters use AND semantics)
meetings = client.meetings.list_meetings(
    search="pricing model",
    organization_id=5,
    start_after="2024-01-01",
)
```

Ranking is tiered: title and calendar-summary hits rank above summary-only
hits, which rank above tag-only hits. Summary matching is a contiguous
substring match, so `"budget Q3"` will not match a summary reading
`"Q3 budget"` — title and calendar matching does handle non-adjacent words.

### Get Meeting Details

```python
detail = client.meetings.get_meeting(123)
print(f"Title: {detail.title}")
print(f"Host: {detail.host.name} ({detail.host.email})")
print(f"Location: {detail.location}")
print(f"Language: {detail.language}")
print(f"Keywords: {', '.join(detail.keywords)}")
print(f"Participants: {detail.participant_count}")
print(f"Duration: {detail.duration_seconds}s")

# Calendar event info (if from calendar integration)
if detail.calendar_event:
    print(f"Calendar: {detail.calendar_event.summary}")
    print(f"Meeting Link: {detail.calendar_event.meeting_link}")
```

### Get Meeting Transcript

```python
transcript = client.meetings.get_transcript(123)
print(f"Total segments: {transcript.total_segments}")

for segment in transcript.transcripts:
    minutes = int(segment.start_time // 60)
    seconds = int(segment.start_time % 60)
    print(f"[{minutes:02d}:{seconds:02d}] {segment.speaker}: {segment.text}")
```

### Get Meeting Summary

```python
summary = client.meetings.get_summary(123)
print(summary.summary)  # Markdown or legacy JSON
print(f"Notes: {summary.notes}")
print(f"Keywords: {', '.join(summary.keywords)}")

# Handle both new Markdown and legacy JSON formats
if isinstance(summary.summary, str):
    # New Markdown format
    print(summary.summary)
elif isinstance(summary.summary, dict):
    # Legacy JSON format
    print(summary.summary.get("ringkasan", ""))
```

### Get Meeting Summary With Video

Converts the Meemo `(segment:id)` citations in a summary into video timestamp
references for messaging UIs (e.g. GLChat). For raw summary text and keywords,
use `get_summary`.

GLChat has **two mutually exclusive contracts**. Send the field that matches who
is sending the message:

| Sender | Field to send | `media_mapping` |
|--------|---------------|-----------------|
| AIP agent | `inline_message_markdown` (verbatim) | ignore it |
| Native GLChat pipeline | `placeholder_message_markdown` | send it |

An agent cannot supply a `media_mapping`: GLChat derives one from the agent's own
text and mints keys the agent cannot predict, so bracket references coming from
an agent resolve to nothing and are dropped **without an error**. That is why the
agent shape inlines the media markdown per citation instead.

On multi-record meetings the summary here can differ from `get_summary()`, which
always reads the first record. Check `record_id` to see which recording the
citations and video URL are measured against.

```python
rich = client.meetings.get_summary_with_video(123)

print(rich.record_id)  # e.g. 42 — which recording this export describes
print(rich.video_id)  # "video_meeting_123"
print(rich.has_video)        # True when the meeting has a recording (either contract)

# What an AIP agent sends, character for character.
print(rich.inline_message_markdown)

# What a native GLChat pipeline sends, paired with media_mapping.
print(rich.placeholder_message_markdown)
print(rich.media_mapping)

# Resolved citations, unique per distinct set of segment ids.
for citation in rich.citations:
    print(citation.segment_ids, citation.placeholder)

# Convenience: the bare recording URL, parsed out of media_mapping.
# Can be None even when has_video is True, so null-check it before dereferencing.
print(rich.video_url)
```

`has_video` answers "does a recording exist under either contract"; `video_url`
answers "did the pipeline mapping resolve to a URL the SDK could parse". A
`media_mapping` keyed for another `video_id`, or whose value is not `![](...)`
markdown, leaves `has_video` `True` with `video_url` unset.

Both message shapes lead with the video player and are followed by the summary
body. Exactly one player is emitted — a second would register under the same
element key in GLChat and leave the timestamp buttons seeking the wrong video.

Media URLs are signed and time-limited, and are regenerated on every call, so
fetch this close to when the message is actually sent. When the meeting has no
recording, citations degrade to plain labels rather than dead references.

### Get Meeting Participants

```python
participants = client.meetings.get_participants(123)
print(f"Total: {participants.total_participants}")

for p in participants.participants:
    if p.id:
        print(f"  {p.name} ({p.email}) - {p.position}, {p.department}")
    else:
        print(f"  {p.name} (external guest)")
```

### Get Meeting Recording

```python
recording = client.meetings.get_recording(123)

# Preferred: iterate over assets (audio and/or video).
if recording.assets:
    for asset in recording.assets:
        print(f"{asset.media_kind}: {asset.url} ({asset.format})")
    print(f"Duration: {recording.duration}s")
else:
    print("No recording available")

# Deprecated back-compat aliases (audio only), populated from the first
# audio asset when not provided directly:
if recording.recording_url:
    print(f"Audio URL: {recording.recording_url} ({recording.format})")

# Convenience aliases for the first video asset when present:
if recording.video_url:
    print(f"Video URL: {recording.video_url} ({recording.video_format})")
```

## Multi-Organization Support

External applications can access meetings from multiple organizations with a single set of credentials.

```python
# Get meetings from all accessible organizations
all_meetings = client.meetings.list_meetings()

# Filter to a specific organization
org_meetings = client.meetings.list_meetings(organization_id=5)
```

## Error Handling

```python
import httpx
from meemo import MeemoClient

client = MeemoClient(
    client_id="your-client-id",
    client_secret="your-client-secret",
    base_url="https://api-meemo.glair.ai",
)

try:
    detail = client.meetings.get_meeting(999)
except httpx.HTTPStatusError as e:
    if e.response.status_code == 401:
        print("Authentication failed - check your credentials")
    elif e.response.status_code == 404:
        print("Meeting not found")
    else:
        print(f"HTTP Error: {e.response.status_code}")
        print(f"Response: {e.response.text}")
except ValueError as e:
    print(f"Validation Error: {e}")
```

## Testing

### Unit tests

```bash
cd libs/meemo
uv sync --group dev
uv run pytest tests/unit_tests/ -v
```

### Integration tests (live)

The integration tests exercise a real Meemo instance. Copy `.env.example` to `.env`, fill in your Meemo OAuth credentials, then:

```bash
uv run pytest tests/integration_tests/ -v
```

Optional flags in `.env` or the shell:

- `MEEMO_TEST_CREATE_MEETING=1` — also run the `create_meeting` live test (leaves a real meeting behind; off by default)

## API Reference

- [Meemo External API Documentation](https://gdplabs.gitbook.io/meemo/resources/external-api-documentation)

## License

MIT License.
