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
pip install meemo
Or with uv:
uv add meemo
Quick Start
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:
recording = meeting.recording.status if meeting.recording else "n/a"
print(f"{meeting.id}: {meeting.title} ({meeting.status}, recording={recording})")
# Get meeting details
detail = client.meetings.get_meeting(123)
print(f"Host: {detail.host.name}")
if detail.duration_seconds is not None:
print(f"Duration: {detail.duration_seconds}s")
else:
print("Duration: still in progress")
if detail.recording:
print(f"Recording: {detail.recording.status} ({detail.recording.description})")
# 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:
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 authenticationMEEMO_CLIENT_SECRET: OAuth2 client secret for authenticationMEEMO_BASE_URL: Base URL of the Meemo instance
Client Initialization
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).
# Token management is automatic, but you can also manage it manually:
# Revoke the current token
client.revoke_token()
Meetings API
List Meetings
# 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.
# 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.
Filter by meeting link, calendar name, or description
These filters are case-insensitive contains matches against calendar metadata
(and, for meeting_link, also bot_meeting_url). They can return many rows;
combine them with other filters using AND semantics. A present-but-blank value
matches nothing.
Requires an api-service build that carries the calendar lookup filters (Meemo
beta). An older build drops them as unknown query params and answers with an
unfiltered list, which is indistinguishable from "everything matched" — unlike
find_meeting_by_calendar_event_id, these filters do not re-check the returned
rows for you. To probe a build, query a value that cannot exist (a random UUID):
a build that carries the filter answers with an empty page.
# By conference URL (full URL or a meet code such as "abc-defg-hij")
meetings = client.meetings.list_meetings(
meeting_link="https://meet.google.com/abc-defg-hij",
)
# By calendar event name/summary
meetings = client.meetings.list_meetings(calendar_event_name="Q1 Planning")
# By calendar event description
meetings = client.meetings.list_meetings(calendar_event_description="budget review")
A query longer than the stored link (for example the same Meet URL with
?authuser=0 appended) matches nothing. A generic substring such as
meet.google.com can match many meetings.
Get Meeting Details
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}")
if detail.duration_seconds is not None:
print(f"Duration: {detail.duration_seconds}s")
else:
print("Duration: still in progress (end_time is null until the meeting ends)")
if detail.recording:
print(f"Recording: {detail.recording.status} — {detail.recording.description}")
if detail.transcription:
print(f"Transcription: {detail.transcription.status} — {detail.transcription.description}")
if detail.summarization:
print(f"Summarization: {detail.summarization.status} — {detail.summarization.description}")
# 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}")
print(f"Scheduled end: {detail.calendar_event.end_time}")
end_time and duration_seconds stay None until the meeting actually ends. For bot meetings, start_time is when the bot joined and end_time is when it left or failed. The calendar invite's scheduled end is on calendar_event.end_time, not the meeting's end_time.
recording, transcription, and summarization each have a status and a readable description. For bot meetings, recording follows the bot (pending while joining or waiting, in progress in the call, completed after it leaves). They are optional so older api-service builds that omit them still parse.
These are processing progress, not the media: detail.recording.status == "completed" says the recording finished, not where to download it — use get_recording() for the file itself.
Compare a status against the exported StageStatus enum rather than a bare string:
from meemo import StageStatus
if detail.transcription and detail.transcription.status == StageStatus.FAILED:
print(f"Transcription failed: {detail.transcription.description}")
StageStatus is a StrEnum with PENDING, IN_PROGRESS, COMPLETED, and FAILED, so it compares equal to the raw strings the API sends.
Get Meeting Transcript
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
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.
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
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
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.
# 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
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
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:
uv run pytest tests/integration_tests/ -v
Optional flags in .env or the shell:
MEEMO_TEST_CREATE_MEETING=1— also run thecreate_meetinglive test (leaves a real meeting behind; off by default)
API Reference
License
MIT License.
Metadata
Release files for meemo 0.0.9
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| meemo-0.0.9.tar.gz | 36.6 kB | Details |
Release files / meemo-0.0.9.tar.gz
| Download URL | meemo-0.0.9.tar.gz |
|---|---|
| Size | 36.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c85e4164121624cf199845776c076f4b1c02584311783187581dc0ad5d8b7375
|
|
BLAKE2b-256 checksum How to use checksums |
88acd1ed87fa118a21d0899cb6a80e09c63c5592fc70b5dc90633a1281aa082f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.8.24
|