Python client for the Mobiscroll Connect API — calendar and event management across Google, Microsoft, Apple, and CalDAV.
Project description
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.
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_alltraverses every page) - Type-checked (
py.typedshipped)
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
)
# 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",
})
Connection management
status = client.auth.get_connection_status()
for provider, accounts in status.connections.items():
print(f"{provider}: {len(accounts)} account(s)")
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)
└── aio/
├── client.py — AsyncMobiscrollConnectClient
└── resources.py — AsyncAuth / AsyncCalendars / AsyncEvents
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).
httpxfor both sync and async. Single dependency, identical request API.requestswould force a separate sync transport.asyncio.Lockandthreading.Lockfor refresh dedup. Concurrent 401s wait on the same in-flight refresh instead of racing — same invariant as the Node SDK'srefreshTokenPromise.- Resources as attributes (
client.auth, notclient.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 managenext_page_tokenby hand; Python iterators are the natural shape and remove the bookkeeping.
Testing
pip install -e ".[dev]"
pytest
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file mobiscroll_connect_sdk-1.0.1.tar.gz.
File metadata
- Download URL: mobiscroll_connect_sdk-1.0.1.tar.gz
- Upload date:
- Size: 33.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2cdb69ccf8f6e4c29107de779258491f0458e89fa26db7d03ca2dd3861e0bcb9
|
|
| MD5 |
9b439e3b93e044eb118cd2c581f725a5
|
|
| BLAKE2b-256 |
33d3b90f775475a307728d5767f2e38296a24cf72e4a884e73d44897fd0b578e
|
Provenance
The following attestation bundles were made for mobiscroll_connect_sdk-1.0.1.tar.gz:
Publisher:
release-python.yml on acidb/mobiscroll-connect-sdks
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
mobiscroll_connect_sdk-1.0.1.tar.gz -
Subject digest:
2cdb69ccf8f6e4c29107de779258491f0458e89fa26db7d03ca2dd3861e0bcb9 - Sigstore transparency entry: 1516975690
- Sigstore integration time:
-
Permalink:
acidb/mobiscroll-connect-sdks@18501d0ed684965c7b95051f1ecc469c4f5caf25 -
Branch / Tag:
refs/tags/python-v1.0.1 - Owner: https://github.com/acidb
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release-python.yml@18501d0ed684965c7b95051f1ecc469c4f5caf25 -
Trigger Event:
push
-
Statement type:
File details
Details for the file mobiscroll_connect_sdk-1.0.1-py3-none-any.whl.
File metadata
- Download URL: mobiscroll_connect_sdk-1.0.1-py3-none-any.whl
- Upload date:
- Size: 24.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7dc6f061b203536fe26433ce0641705f355922b4b223bddd3e8ced1fbb3b9659
|
|
| MD5 |
b4358eaec6ea8de88d0864ac764e9097
|
|
| BLAKE2b-256 |
f1312e5ca4d08dbfdadf749bb3de7f60589391d9a1cfa2d780ced34045b879e8
|
Provenance
The following attestation bundles were made for mobiscroll_connect_sdk-1.0.1-py3-none-any.whl:
Publisher:
release-python.yml on acidb/mobiscroll-connect-sdks
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
mobiscroll_connect_sdk-1.0.1-py3-none-any.whl -
Subject digest:
7dc6f061b203536fe26433ce0641705f355922b4b223bddd3e8ced1fbb3b9659 - Sigstore transparency entry: 1516975878
- Sigstore integration time:
-
Permalink:
acidb/mobiscroll-connect-sdks@18501d0ed684965c7b95051f1ecc469c4f5caf25 -
Branch / Tag:
refs/tags/python-v1.0.1 - Owner: https://github.com/acidb
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release-python.yml@18501d0ed684965c7b95051f1ecc469c4f5caf25 -
Trigger Event:
push
-
Statement type: