Skip to main content

Pipe-friendly calendar CLI for Outlook / Microsoft 365

Project description

owa-cal

Calendar CLI for Outlook / Microsoft 365. Read, create, update and delete events from the terminal. Pipe-friendly JSON by default, --pretty for humans.

brew install damsleth/tap/owa-cal
owa-cal events --pretty

Happy-path setup (no app registration)

owa-piggy owns the token lifecycle; owa-cal just shells out to it on every call. The full first-run flow:

# 1. Install both
brew install damsleth/tap/owa-piggy damsleth/tap/owa-cal

# 2. Seed owa-piggy once from your browser (walks you through it)
owa-piggy setup

# 3. Go
owa-cal events --pretty

Multi-account: seed a named owa-piggy profile and pin it in owa-cal's config.

owa-piggy setup --profile work
owa-cal config --profile work

--profile also works as a one-shot override: owa-cal --profile home events.

Refresh tokens rotate on every call and are persisted by owa-piggy in its own profile store. owa-cal stores no refresh token on this path; owa-piggy --reseed --profile <alias> refreshes the token headlessly when the 24h hard-expiry lapses, and owa-cal picks up the new token on the next call automatically.


The output contract

JSON on stdout, logs on stderr. Every read command emits parseable JSON by default; --pretty is a human override that goes to stdout too. That means the entire CLI composes with jq:

owa-cal events
[
  {
    "id": "AAMkAGI1...redacted",
    "subject": "Standup",
    "start": "2026-04-20T09:00:00",
    "end": "2026-04-20T09:30:00",
    "categories": ["ProjectX"],
    "location": "Teams",
    "showAs": "Busy",
    "isAllDay": false
  },
  {
    "id": "AAMkAGI2...redacted",
    "subject": "Lunsj",
    "start": "2026-04-20T11:00:00",
    "end": "2026-04-20T11:30:00",
    "categories": ["CC LUNCH"],
    "location": "",
    "showAs": "Busy",
    "isAllDay": false
  }
]

Timestamps are normalized to your local timezone. Field names in the output are stable lowercase; the backend is Outlook REST v2 (PascalCase upstream) but owa-cal hides that detail.

owa-cal events | jq '.[].subject'
owa-cal events --date tomorrow | jq '[.[] | select(.showAs == "Busy")] | length'
owa-cal events --week 16 | jq 'group_by(.start | .[0:10]) | map({day: .[0].start[0:10], count: length})'

Same shape on create / update (returns the single normalized event), and on categories (returns [{"name": ..., "color": ...}]).


Commands

owa-cal events --pretty                       # today
owa-cal events --week 16 --pretty             # ISO week
owa-cal events --from 2026-04-14 --to 2026-04-18 --pretty
owa-cal events --search "standup" --pretty

owa-cal create --subject "lunsj" --start 11:00 --end 11:30 --category "CC LUNCH"
owa-cal update --id <event-id> --category "ProjectX"
owa-cal delete --id <event-id>

owa-cal categories                            # JSON
owa-cal categories --pretty                   # aligned table

Auth

Two paths:

  • owa-piggy bridge (default) - owa-cal shells out to owa-piggy, which piggybacks on OWA's public SPA client. No app registration needed; owa-cal stores no refresh token. Optional owa_piggy_profile pins a named owa-piggy profile.
  • With an app registration - set OUTLOOK_APP_CLIENT_ID, OUTLOOK_REFRESH_TOKEN, and OUTLOOK_TENANT_ID in the config file and owa-cal talks to the AAD token endpoint directly.

Config lives at ~/.config/owa-cal/config:

# Default (owa-piggy) path - optional, pins a profile alias
owa_piggy_profile="work"

# App-registration path (optional, mutually exclusive)
OUTLOOK_APP_CLIENT_ID=""
OUTLOOK_REFRESH_TOKEN=""
OUTLOOK_TENANT_ID=""

OUTLOOK_APP_CLIENT_ID can be overridden via the environment. The refresh token / tenant id on the app-registration path live exclusively in the config file.


Dependencies

  • Python 3.8+ (stdlib only - no pip install required at runtime)
  • owa-piggy unless you bring your own app registration

Development

git clone https://github.com/damsleth/owa-cal
cd owa-cal
pip install -e '.[test]'
pytest -q

See AGENTS.md for repo layout and ground rules.

Disclaimer

Personal tooling. Stores a delegated refresh token on disk.
If you don't know why that might be a bad idea, don't use it.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

owa_cal-0.6.1.tar.gz (28.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

owa_cal-0.6.1-py3-none-any.whl (23.1 kB view details)

Uploaded Python 3

File details

Details for the file owa_cal-0.6.1.tar.gz.

File metadata

  • Download URL: owa_cal-0.6.1.tar.gz
  • Upload date:
  • Size: 28.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.10.10 {"installer":{"name":"uv","version":"0.10.10","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for owa_cal-0.6.1.tar.gz
Algorithm Hash digest
SHA256 ea3625b65611f68327f9b2dcfc6d979abc9addd8a62de7ad24888a111d85bf1e
MD5 42aebfb4edfa252f18aea4afe7929a41
BLAKE2b-256 8b3c810120cd6ebe33efea6421a2e2a888338cec67a2688bc05603d4cd17926f

See more details on using hashes here.

File details

Details for the file owa_cal-0.6.1-py3-none-any.whl.

File metadata

  • Download URL: owa_cal-0.6.1-py3-none-any.whl
  • Upload date:
  • Size: 23.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.10.10 {"installer":{"name":"uv","version":"0.10.10","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for owa_cal-0.6.1-py3-none-any.whl
Algorithm Hash digest
SHA256 5f6f4f0e1b6cf19ca37a9a141e84608827f8aa60cb9421afb7aac2c6161662b4
MD5 386effd190d1cf9d49bf71ebd9eaffc5
BLAKE2b-256 15db5d7bde7c3ad3d0eb582994e3882f55108adfe3d6f1f500bb662de128680c

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page