Skip to main content

Mobiscroll Connect Python SDK

Python client for the Mobiscroll Connect API — calendar and event management across Google Calendar, Microsoft Outlook, Apple Calendar, and CalDAV through a single SDK.

📖 Full documentation

Features

  • Multi-provider: Google, Microsoft, Apple, CalDAV
  • OAuth2: full authorization-code flow
  • Automatic token refresh with persistence callback
  • Sync and async clients (MobiscrollConnectClient / mobiscroll_connect.aio.AsyncMobiscrollConnectClient)
  • Typed responses via frozen dataclasses
  • Typed exception hierarchy for HTTP errors
  • Pagination helpers (iter_all traverses every page)
  • Type-checked (py.typed shipped)

Installation

pip install mobiscroll-connect-sdk

Requires Python 3.9+.

Quick start

from mobiscroll_connect import MobiscrollConnectClient

with MobiscrollConnectClient(
    client_id="YOUR_CLIENT_ID",
    client_secret="YOUR_CLIENT_SECRET",
    redirect_uri="https://yourapp.example/oauth/callback",
) as client:
    # 1. Build the auth URL and redirect the user
    auth_url = client.auth.generate_auth_url(user_id="user-123")

    # 2. After callback: exchange the code for tokens
    tokens = client.auth.get_token(code="...")

    # 3. Use the API
    for calendar in client.calendars.list():
        print(calendar.provider, calendar.title)

OAuth2 flow

# Step 1 — generate auth URL (server-side)
auth_url = client.auth.generate_auth_url(
    user_id="user-123",
    scope="calendar",       # optional
    state="csrf-value",     # optional
    providers="google,microsoft",  # optional
    lng="es",               # optional: Connect page language, see https://mobiscroll.com/docs/connect/localization#supported-languages
)

# Step 2 — exchange the code (in your callback handler)
tokens = client.auth.get_token(code=request.query_params["code"])

# Persist tokens.access_token, tokens.refresh_token, tokens.expires_in

# Step 3 — restore credentials on subsequent requests
from mobiscroll_connect import TokenResponse

client.auth.set_credentials(TokenResponse(
    access_token=session["access_token"],
    refresh_token=session["refresh_token"],
    expires_in=session["expires_in"],
))

Automatic token refresh

When a request returns 401 Unauthorized and a refresh token is present, the SDK transparently refreshes and retries. Register a callback to persist the new tokens:

def persist_tokens(tokens):
    db.update_tokens(user_id, tokens.to_dict())

client.on_tokens_refreshed(persist_tokens)

If the refresh itself fails (revoked, expired), AuthenticationError is raised — re-authorize the user.

Calendars

calendars = client.calendars.list()
for cal in calendars:
    print(f"{cal.provider}: {cal.title} ({cal.id})")

Events

List events

from datetime import datetime

response = client.events.list(
    start=datetime(2024, 1, 1),
    end=datetime(2024, 1, 31),
    calendar_ids={"google": ["primary"]},
    page_size=50,
)

for event in response:           # EventsListResponse is iterable
    print(event.title, event.start, event.end)

if response.has_more:
    next_page = client.events.list(
        next_page_token=response.next_page_token,
        page_size=50,
    )

Iterate all pages

for event in client.events.iter_all(
    start=datetime(2024, 1, 1),
    end=datetime(2024, 12, 31),
    page_size=250,
):
    process(event)

Create

event = client.events.create({
    "provider": "google",
    "calendar_id": "primary",
    "title": "Team Meeting",
    "start": "2024-06-15T10:00:00Z",
    "end": "2024-06-15T11:00:00Z",
    "description": "Quarterly review",
    "location": "Conference Room A",
})
print("Created:", event.id)

Update

client.events.update({
    "provider": "google",
    "calendar_id": "primary",
    "event_id": "evt-123",
    "title": "Team Meeting (Rescheduled)",
    "start": "2024-06-15T14:00:00Z",
    "end": "2024-06-15T15:00:00Z",
})

Delete

client.events.delete({
    "provider": "google",
    "calendar_id": "primary",
    "event_id": "evt-123",
})

Recurring events

# Update only this instance
client.events.update({
    "provider": "google",
    "calendar_id": "primary",
    "event_id": "instance-id",
    "recurring_event_id": "series-id",
    "update_mode": "this",
    "title": "One-off change",
})

# Delete this and all following instances
client.events.delete({
    "provider": "google",
    "calendar_id": "primary",
    "event_id": "instance-id",
    "recurring_event_id": "series-id",
    "delete_mode": "following",
})

Webhooks

# Subscribe to change notifications for a calendar
subscription = client.webhooks.subscribe_webhook("google", "primary")
print(subscription.channel_id, subscription.subscription.resource_id)

# ...persist subscription.channel_id (and subscription.subscription.resource_id
# for Google) so you can unsubscribe later...

# Unsubscribe when you're done
client.webhooks.unsubscribe_webhook(
    "google",
    subscription.channel_id,
    resource_id=subscription.subscription.resource_id,
)

Connection management

status = client.auth.get_connection_status()
for provider, accounts in status.connections.items():
    print(f"{provider}: {len(accounts)} account(s)")

    # Google's consent screen lets the user untick the calendar permission and still
    # finish signing in. Such an account is connected but lists no calendars.
    for account in accounts:
        if account.calendar_permission_granted is False:
            print(f"  {account.id} must reconnect and allow calendar access")

if status.limit_reached:
    print(f"Connection limit of {status.limit} reached")

# Disconnect a single account
client.auth.disconnect("google", account="user@gmail.com")

# Or all accounts of a provider
client.auth.disconnect("microsoft")

Async usage

import asyncio
from mobiscroll_connect.aio import AsyncMobiscrollConnectClient

async def main():
    async with AsyncMobiscrollConnectClient(
        client_id="...",
        client_secret="...",
        redirect_uri="...",
    ) as client:
        await client.auth.get_token(code="...")
        async for event in client.events.iter_all(start="2024-01-01", end="2024-01-31"):
            print(event.title)

asyncio.run(main())

Error handling

Exception HTTP status Extra
AuthenticationError 401, 403
ValidationError 400, 422 .details
NotFoundError 404
RateLimitError 429 .retry_after
ServerError 5xx .status_code
NetworkError — (transport)

All errors inherit from MobiscrollConnectError.

from mobiscroll_connect import (
    AuthenticationError, ValidationError, NotFoundError,
    RateLimitError, ServerError, NetworkError, MobiscrollConnectError,
)

try:
    client.events.list()
except AuthenticationError:
    # Refresh failed — re-authorize the user
    ...
except ValidationError as e:
    print(e.details)
except RateLimitError as e:
    print(f"Retry after {e.retry_after}s")
except ServerError as e:
    print(f"Server returned {e.status_code}")
except NetworkError:
    # Connection / DNS / timeout
    ...
except MobiscrollConnectError:
    # Catch-all
    ...

Architecture

mobiscroll_connect/
├── __init__.py                — public re-exports
├── client.py                  — MobiscrollConnectClient (sync entry point)
├── api_client.py              — sync HTTP layer + token refresh
├── async_api_client.py        — async HTTP layer + token refresh
├── config.py                  — frozen Config dataclass
├── exceptions.py              — exception hierarchy
├── models.py                  — frozen dataclass response models
├── _internal/
│   ├── errors.py              — HTTP → exception mapper (shared)
│   └── payloads.py            — query/payload builders (shared)
├── resources/
│   ├── auth.py                — Auth (sync)
│   ├── calendars.py           — Calendars (sync)
│   ├── events.py              — Events (sync)
│   └── webhooks.py            — Webhooks (sync)
└── aio/
    ├── client.py              — AsyncMobiscrollConnectClient
    └── resources.py           — AsyncAuth / AsyncCalendars / AsyncEvents / AsyncWebhooks

Why these choices

  • Frozen dataclasses, not Pydantic. No third-party runtime dependency for models — matches the "stdlib-only DTOs" approach of the PHP and Node SDKs and keeps install size small. Validation is done where it matters (response parsing, query builders).
  • httpx for both sync and async. Single dependency, identical request API. requests would force a separate sync transport.
  • asyncio.Lock and threading.Lock for refresh dedup. Concurrent 401s wait on the same in-flight refresh instead of racing — same invariant as the Node SDK's refreshTokenPromise.
  • Resources as attributes (client.auth, not client.auth()). Idiomatic Python; the parens-method style in the PHP SDK exists only because PHP can't expose readonly properties cleanly.
  • Pagination helper (iter_all). PHP/Node make callers manage next_page_token by hand; Python iterators are the natural shape and remove the bookkeeping.

Testing

pip install -e ".[dev]"
pytest

License

MIT

Release files for mobiscroll-connect-sdk 1.5.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for mobiscroll-connect-sdk 1.5.0
File Size Uploaded
mobiscroll_connect_sdk-1.5.0.tar.gz 38.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for mobiscroll-connect-sdk 1.5.0
File Interpreter ABI Platform
mobiscroll_connect_sdk-1.5.0-py3-none-any.whl Python 3 none any Details

Total release size: 66.9 kB

Release files / mobiscroll_connect_sdk-1.5.0.tar.gz

Download URL mobiscroll_connect_sdk-1.5.0.tar.gz
Size 38.2 kB
Tags Source
SHA-256 checksum
How to use checksums
943d07e0e74ea31fc2ce7e8d93b974b9dd2279cf3ffa72c727edde66bf52dfec
BLAKE2b-256 checksum
How to use checksums
b44cf04ac0f2d97baf0359a32cd4bb2c39155068277d7c84e434dd29cfa1b717
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 22, 2026.

Transparency log

Release files / mobiscroll_connect_sdk-1.5.0-py3-none-any.whl

Download URL mobiscroll_connect_sdk-1.5.0-py3-none-any.whl
Size 28.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
93212eda46d4a1b8b92c6ec694e805f1f6100df5c42a079c2f45d3112302a543
BLAKE2b-256 checksum
How to use checksums
b6f4b988d6a8dfb446c95616ba6431cbcad284a40040ed134d50c587372311f6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 22, 2026.

Transparency log

Release history Release notifications | RSS feed

1.5.1

2 release files

This release

1.5.0 This release

2 release files

1.2.0

2 release files

1.1.1

2 release files

1.1.0

2 release files

1.0.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page