Skip to main content

Mirrors events between calendars as anonymized busy blocks, using your local macOS calendar store (EventKit).

Project description

calque

Build Quality Gate Status Coverage

Mirrors events between calendars as anonymized busy blocks, using your local macOS calendar store. Built for the case where your availability has to show up on calendars in different tenants (e.g. keeping a client Exchange calendar and a your company Google calendar in sync) without exposing undesirable event detail.

By design, this is not a cloud service. Because calendars are already subscribed in macOS, calque talks only to the local EventKit store. There is no authentication required against any of the calendar providers, and no use of their APIs. This makes it suitable for use with government clients and other sensitive tenants that forbid third-party tool access.

What it does

  • Reads events from the calendars you specify, in a configurable time window around the current time. The first calendar you list is the primary; the rest are mirrored against it.
  • Mirrors only the events you've accepted (tentative, declined, and unanswered are skipped by default). The set of statuses that count as busy is configurable.
  • Drops any event an exclusion rule rejects: by title pattern, time, or because the target calendar is already busy over that slot (see Exclusions).
  • Writes one block per mirrored event with a templated title (default Busy ({account} calendar)); no attendees, no joining into, , no location, no notes beyond a hidden opaque marker.
  • Collects then fans out: each calendar's events are mirrored into the primary, so the primary acts as a hub and holds the full picture, and the primary is then mirrored back out to each of the others, so time you've committed with one client shows as busy (anonymized) to the rest. Pass --mute <CALENDAR> to keep a calendar from receiving blocks while still reading it as a source.
  • Re-runs are idempotent: each block carries a hidden marker linking it to its source event and origin calendar, so calque updates times that moved, removes blocks whose source disappeared, and never mirrors a block back to the calendar it came from.

Getting started

Prerequisites

  • macOS
  • every calendar you want to sync already subscribed in Calendar.app.
  • Python 3.13 or later.
  • Something to install Python packages: calque is a standard Python package — uv, pipx or pip all install it.

pipx is recommended for a command-line tool, as it puts calque in its own isolated environment and on your PATH:

pipx install calque

To install from a checkout of this repository, point any of them at the directory, e.g. uv tool install .

Grant Access and View Calendars

EventKit is gated by macOS privacy controls. Run calque with --list-calendars once interactively: the first run triggers a permission prompt; you grant access via a Ui dialog which is thereafter managed under System Settings → Privacy & Security → Calendars for the app running calque. In the first instance, this is your terminal. If you install calque as a launchd agent, the agent will not run under the the terminal and will ask separately for permission. Once granted, you can list every calendar's account-qualified name with:

calque --list-calendars

The output is every calendar's account-qualified name (Account.Calendar). This is an example of the output you might see:

Acme Consulting.Acme Consulting
Acme Consulting.Holidays in the United Kingdom
MoM.Calendar
MoSW.Birthdays
MoSW.Calendar
MoSW.United Kingdom holidays
Other.Birthdays
Subscribed Calendars.UK Holidays
iCloud.Home
iCloud.Personal

These qualified names are the form calque accepts: use them exactly as printed wherever a calendar is named, quoting any that contain spaces.

Dry Run

Always dry-run first: calque logs the exact plan (what it would create, update, and delete in each calendar) and writes nothing. The first calendar is the primary; the rest are mirrored against it.

calque "Acme Consulting.Acme Consulting" "MoSW.Calendar" "MoM.Calendar" "iCloud.Home" \
  --title-to "Acme Consulting.Acme Consulting" "{account}: {title}" \
  --title-from "iCloud.Home" "Busy" \
  --mute "iCloud.Home" \
  --dry-run

This is a typical consultant setup. Acme Consulting.Acme Consulting is your company calendar (the hub), MoSW.Calendar and MoM.Calendar are two client tenants, and iCloud.Home is your personal calendar. It mirrors all three into your company calendar, then fans your company calendar's combined availability back out to the two clients:

  • --title-to "Acme Consulting.Acme Consulting" "{account}: {title}": in your own company diary, show the real source account and subject, so you and your colleagues can see what each block actually is and where you are busy.
  • --title-from "iCloud.Home" "Busy": anything originating from your personal calendar is labelled just Busy wherever it lands, so home detail never leaks — not even into your company diary, which the --title-to rule would otherwise make detailed (a source override wins over a target one).
  • Client calendars (MoSW.Calendar, MoM.Calendar) see only the default opaque Busy (Acme Consulting calendar) blocks, so neither sees the other's events, your home detail, or anything beyond the fact that you're busy.
  • --mute "iCloud.Home" — read your personal calendar as a source but never write blocks into it; it stays untouched.

The two clients (MoSW.Calendar, MoM.Calendar) receive only the default opaque Busy (…) blocks, so neither sees the other's events, your home detail, or anything beyond the fact that you're busy.

When the plan looks right, drop --dry-run to apply it once, or install it as an agent to keep both sides in sync (next step).

Install Agent

Schedule calque to run automatically with launchd. calque installs its own lanunchd agent: pass --install SECONDS with the same arguments you previously (dry-)ran, and it writes and loads the agent for you (no plist to edit):

When the agent first runs from launchd, it will prompt for calendar access via a system dialog. This will show up as python3.x this time, instead of terminal. You will also get a notification of Managed Login Items Added to indicate that the agent has been installed; this will show as calque in System Settings → General → Login Items.

calque "Acme Consulting.Acme Consulting" "MoSW.Calendar" "MoM.Calendar" iCloud.Home \
  --title-to "Acme Consulting.Acme Consulting" "{account}: {title}" \
  --title-from "iCloud.Home" "Busy" \
  --mute "iCloud.Home" \
  --install 180          # run every 3 minutes

Remove it with:

calque --uninstall

Options

  • calendars (positional, two or more) — the calendars to sync, each a qualified name from --list-calendars. The first is your primary calendar: every other is mirrored into it, and it is then mirrored back out to each.
  • --title TEMPLATE — default title template for every mirror block (default Busy ({account} calendar)). {field} placeholders are filled from the source event; see Titles.
  • --title-to NAME TEMPLATE — title template used when writing into NAME's calendar. Repeatable.
  • --title-from NAME TEMPLATE — title template for events read from NAME's calendar, wherever they land; wins over --title-to. Repeatable.
  • --mute NAME … — calendars to read as a source but never write blocks into.
  • --lookback DAYS — days before now to keep mirrored (default 1). With --cleanup, this instead bounds the window within which finished blocks are removed.
  • --lookahead DAYS — days after now to mirror (default 60).
  • --cleanup / --no-cleanup — remove a mirror block once its event is over, instead of keeping it for the lookback window (default off).
  • --exclude-pattern REGEX … — replace the default title-exclusion patterns (see Exclusions).
  • --exclude-clashes / --no-exclude-clashes — skip a source event that overlaps a genuine event already on the target (default on).
  • --exclude-all-day / --no-exclude-all-day — skip all-day events (default on).
  • --exclude-out-of-hours / --no-exclude-out-of-hours — skip events that fall entirely outside working hours (default on).
  • --dry-run — log the plan without writing any changes.
  • --list-calendars — print every calendar's account-qualified name and exit.
  • --install SECONDS — install a launchd agent that runs this same command every SECONDS, then exit.
  • --uninstall — remove the installed launchd agent and exit.
  • --logging LEVEL — logging level (default info; debug shows why each event was kept or excluded). Be careful not to place variadic options (--mute, --exclude-pattern) immediately before the calendar arguments, or they will swallow them.

Titles

The mirror-block title is a template. Placeholders are filled from the source event, so {account} is the source account and {title} is the event's real subject. The default, Busy ({account} calendar), doesn't include {title} and so keeps the details opaque. {account} is always the source of the current event, so when mirroring events from the primary calendar into a client calendar, {account} is the primary calendar's account (even if the busy block originated from a different client calendar). This ensures that, if using {account}, auxiliary calendars never see the names of calendars other than the primary (which we assume they already know).

In addition to --title, two overrides control the title used for a calendar, both repeatable and both keyed on a calendar's qualified name:

  • --title-to NAME TEMPLATE — the title used when writing into NAME's calendar ("when putting events into this diary, use this format").
  • --title-from NAME TEMPLATE — the title used for events read from NAME's calendar, wherever they land. A title-from override wins over a title-to one.

Common use-cases:

--title-to lets your own diary show real subjects while every client calendar still sees only opaque busy blocks (and so never sees detail of your own or other clients' events):

--title-from pins how one source always appears, regardless of the target. For example, feed a personal calendar into the company diary to block out home commitments, but keep those entries fully opaque even if a --title-to on that calendar would otherwise make them detailed:

calque "Acme Consulting.Acme Consulting" "MoSW.Calendar" "MoM.Calendar" "iCloud.Home" \
  --title-to "Acme Consulting.Acme Consulting" "{account}: {title}" \
  --title-from "iCloud.Home" "Busy"

Exclusions

Before mirroring, every source event runs through a set of exclusion rules; if any rule rejects it, no busy block is written. The rules are assembled per run in exclusions.py, so adding a new kind is a matter of writing one builder and wiring it into rules.

Current rules:

  • By status — only events with a busy status are mirrored. The default busy status is only accepted, but you can override them on Config.
  • By title — source events whose title matches any --exclude-pattern regular expression are skipped, so availability markers don't get mirrored as meetings. The default excludes a bare Working status block (^Working$) and any annual-leave marker (\bA/L\b). Pass one or more of your own patterns to replace the defaults.
  • All-day — all-day events (out-of-office banners, leave, public holidays) are skipped so they don't blank out the whole day on the other calendar. On by default; --no-exclude-all-day.
  • Out of hours — events that fall entirely outside working hours are skipped, so only your working day is mirrored. The window defaults to Monday–Friday 08:00–18:00 local time; an event that overlaps it at all (e.g. a 17:00–19:00 overrun) is still mirrored. On by default; --no-exclude-out-of-hours. The window itself (work_days, work_start, work_end) is set on Config.
  • By clash — a source event is skipped when the target calendar already has a genuine event (not one of calque's own mirror blocks) overlapping any part of its slot, so calque never stacks a busy block on time you're already committed elsewhere. On by default; turn it off with --no-exclude-clashes.

Example: skip any event whose title is exactly Working, contains A/L as a whole word, or is Lunch, and don't skip events that clash with existing events on the target:

calque "Acme Consulting.Acme Consulting" "MoSW.Calendar" \
  --exclude-pattern "^Working$" "\bA/L\b" "\bLunch " --no-exclude-clashes

Writing into every calendar

Availability is mirrored in both directions by default for every calendar you list. If you want to read a calendar but not write into it, pass --mute NAME for that calendar. Note that if you mute the primary calendar, it will mirror its events into the other calendars, but will not not be able to mirror busy blocked between the other calendars - i.e. you have muted the hub so it can't act as a hub for mirroring.

Status filtering

Your response is read from the event's attendee list. Only events you've accepted are mirrored by default (the busy statuses are configurable on Config). Events with no attendees (blocks you created yourself) are treated as implicitly accepted.

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

calque-0.1.0.tar.gz (57.8 kB view details)

Uploaded Source

Built Distribution

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

calque-0.1.0-py3-none-any.whl (23.2 kB view details)

Uploaded Python 3

File details

Details for the file calque-0.1.0.tar.gz.

File metadata

  • Download URL: calque-0.1.0.tar.gz
  • Upload date:
  • Size: 57.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.21 {"installer":{"name":"uv","version":"0.11.21","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for calque-0.1.0.tar.gz
Algorithm Hash digest
SHA256 8f362faba91bda3197019a5ac99f22fa8687cc8029dcdbab3dd52f5b8741f889
MD5 7848d2b367e3820b0f197a685c6fc0c9
BLAKE2b-256 a677300a28837d2a2a4cc7675cbe78a6e51f2d9486c34b42edde79ae50331be3

See more details on using hashes here.

File details

Details for the file calque-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: calque-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 23.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.21 {"installer":{"name":"uv","version":"0.11.21","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for calque-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 53d88757f0c9090134816ebce1c4f22079e0ac728a7839ee575c15b6787cab71
MD5 6f13c6810c9692e450a0a50b5ff88cbf
BLAKE2b-256 1b609136d59cd043765e11f7bdd0b53286a411b914c873ca97782faab0fad657

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