calque 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- calque-0.1.0/.github/workflows/build.yml +21 -0
- calque-0.1.0/.github/workflows/gate.yml +50 -0
- calque-0.1.0/.github/workflows/publish.yml +37 -0
- calque-0.1.0/.gitignore +20 -0
- calque-0.1.0/.python-version +1 -0
- calque-0.1.0/LICENSE +21 -0
- calque-0.1.0/PKG-INFO +275 -0
- calque-0.1.0/README.md +250 -0
- calque-0.1.0/pyproject.toml +134 -0
- calque-0.1.0/sonar-project.properties +6 -0
- calque-0.1.0/src/calque/__init__.py +3 -0
- calque-0.1.0/src/calque/cli.py +200 -0
- calque-0.1.0/src/calque/config.py +69 -0
- calque-0.1.0/src/calque/errors.py +33 -0
- calque-0.1.0/src/calque/exclusions.py +186 -0
- calque-0.1.0/src/calque/model.py +158 -0
- calque-0.1.0/src/calque/py.typed +0 -0
- calque-0.1.0/src/calque/service.py +72 -0
- calque-0.1.0/src/calque/store.py +219 -0
- calque-0.1.0/src/calque/sync.py +77 -0
- calque-0.1.0/tests/test_cli.py +154 -0
- calque-0.1.0/tests/test_config.py +35 -0
- calque-0.1.0/tests/test_errors.py +21 -0
- calque-0.1.0/tests/test_exclusions.py +205 -0
- calque-0.1.0/tests/test_model.py +92 -0
- calque-0.1.0/tests/test_service.py +82 -0
- calque-0.1.0/tests/test_store.py +328 -0
- calque-0.1.0/tests/test_sync.py +167 -0
- calque-0.1.0/uv.lock +458 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
name: Build
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
# Cancel superseded runs on the same ref to save runner time.
|
|
9
|
+
concurrency:
|
|
10
|
+
group: ci-${{ github.ref }}
|
|
11
|
+
cancel-in-progress: true
|
|
12
|
+
|
|
13
|
+
permissions:
|
|
14
|
+
contents: read
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
gate:
|
|
18
|
+
uses: ./.github/workflows/gate.yml
|
|
19
|
+
with:
|
|
20
|
+
sonar: true
|
|
21
|
+
secrets: inherit
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
name: Gate
|
|
2
|
+
|
|
3
|
+
# Reusable quality gate: lint, format, types, tests. Called by build.yml on
|
|
4
|
+
# every push/PR and by publish.yml before a release to PyPI.
|
|
5
|
+
on:
|
|
6
|
+
workflow_call:
|
|
7
|
+
inputs:
|
|
8
|
+
sonar:
|
|
9
|
+
description: Run the SonarQube Cloud scan (requires the SONAR_TOKEN secret).
|
|
10
|
+
type: boolean
|
|
11
|
+
default: false
|
|
12
|
+
|
|
13
|
+
permissions:
|
|
14
|
+
contents: read
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
gate:
|
|
18
|
+
runs-on: macos-latest
|
|
19
|
+
steps:
|
|
20
|
+
- name: Checkout
|
|
21
|
+
uses: actions/checkout@9f698171ed81b15d1823a05fc7211befd50c8ae0 # v6.0.3
|
|
22
|
+
with:
|
|
23
|
+
fetch-depth: 0
|
|
24
|
+
|
|
25
|
+
- name: Set up uv
|
|
26
|
+
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
|
27
|
+
with:
|
|
28
|
+
python-version: "3.14"
|
|
29
|
+
enable-cache: true
|
|
30
|
+
|
|
31
|
+
- name: Install dependencies
|
|
32
|
+
run: uv sync --locked
|
|
33
|
+
|
|
34
|
+
- name: Lint
|
|
35
|
+
run: uv run ruff check .
|
|
36
|
+
|
|
37
|
+
- name: Format
|
|
38
|
+
run: uv run ruff format --check .
|
|
39
|
+
|
|
40
|
+
- name: Types
|
|
41
|
+
run: uv run mypy .
|
|
42
|
+
|
|
43
|
+
- name: Test with coverage
|
|
44
|
+
run: uv run pytest --cov=calque --cov-report=xml --cov-report=term-missing
|
|
45
|
+
|
|
46
|
+
- name: SonarQube scan
|
|
47
|
+
if: inputs.sonar
|
|
48
|
+
uses: SonarSource/sonarqube-scan-action@713881670b6b3676cda39549040e2d88c70d582e # v8.2.0
|
|
49
|
+
env:
|
|
50
|
+
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
gate:
|
|
12
|
+
uses: ./.github/workflows/gate.yml
|
|
13
|
+
|
|
14
|
+
publish:
|
|
15
|
+
needs: gate
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
# The environment lets you require a manual approval before publishing;
|
|
18
|
+
# configure reviewers under Settings -> Environments -> pypi.
|
|
19
|
+
environment:
|
|
20
|
+
name: pypi
|
|
21
|
+
url: https://pypi.org/project/calque/
|
|
22
|
+
permissions:
|
|
23
|
+
id-token: write
|
|
24
|
+
steps:
|
|
25
|
+
- name: Checkout
|
|
26
|
+
uses: actions/checkout@9f698171ed81b15d1823a05fc7211befd50c8ae0 # v6.0.3
|
|
27
|
+
|
|
28
|
+
- name: Set up uv
|
|
29
|
+
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
|
30
|
+
with:
|
|
31
|
+
python-version: "3.14"
|
|
32
|
+
|
|
33
|
+
- name: Build
|
|
34
|
+
run: uv build
|
|
35
|
+
|
|
36
|
+
- name: Publish to PyPI
|
|
37
|
+
run: uv publish --trusted-publishing always
|
calque-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.14
|
calque-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 The calque authors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
calque-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: calque
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Mirrors events between calendars as anonymized busy blocks, using your local macOS calendar store (EventKit).
|
|
5
|
+
Project-URL: Homepage, https://github.com/agilezebra/calque
|
|
6
|
+
Project-URL: Repository, https://github.com/agilezebra/calque
|
|
7
|
+
Project-URL: Issues, https://github.com/agilezebra/calque/issues
|
|
8
|
+
Author-email: Graham Jones <graham@agilezebra.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: availability,busy,calendar,eventkit,macos
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Environment :: MacOS X
|
|
15
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
16
|
+
Classifier: Operating System :: MacOS :: MacOS X
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Topic :: Office/Business :: Scheduling
|
|
22
|
+
Requires-Python: >=3.13
|
|
23
|
+
Requires-Dist: pyobjc-framework-eventkit>=12.2
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# calque
|
|
27
|
+
|
|
28
|
+
[](https://github.com/agilezebra/calque/actions/workflows/build.yml)
|
|
29
|
+
[](https://sonarcloud.io/summary/new_code?id=agilezebra_calque)
|
|
30
|
+
[](https://sonarcloud.io/summary/new_code?id=agilezebra_calque)
|
|
31
|
+
|
|
32
|
+
Mirrors events between calendars as **anonymized busy blocks**, using your **local macOS calendar store**.
|
|
33
|
+
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.
|
|
34
|
+
|
|
35
|
+
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.
|
|
36
|
+
|
|
37
|
+
## What it does
|
|
38
|
+
|
|
39
|
+
- Reads events from the calendars you specify, in a configurable time window around the current time. The **first** calendar
|
|
40
|
+
you list is the **primary**; the rest are mirrored against it.
|
|
41
|
+
- Mirrors only the events you've **accepted** (tentative, declined, and unanswered are skipped by default).
|
|
42
|
+
The set of statuses that count as busy is configurable.
|
|
43
|
+
- Drops any event an **exclusion rule** rejects: by title pattern, time, or because the target
|
|
44
|
+
calendar is already busy over that slot (see [Exclusions](#exclusions)).
|
|
45
|
+
- Writes one block per mirrored event with a **templated title**
|
|
46
|
+
(default `Busy ({account} calendar)`); no attendees, no joining into, , no location, no notes beyond a hidden opaque marker.
|
|
47
|
+
- **Collects then fans out**: each calendar's events are mirrored into the primary, so the primary
|
|
48
|
+
acts as a hub and holds the full picture, and the primary is then mirrored back out to each of the others, so time
|
|
49
|
+
you've committed with one client shows as busy (anonymized) to the rest. Pass `--mute <CALENDAR>` to
|
|
50
|
+
keep a calendar from *receiving* blocks while still reading it as a source.
|
|
51
|
+
- Re-runs are **idempotent**: each block carries a hidden marker linking it to its source event
|
|
52
|
+
and origin calendar, so calque updates times that moved, removes blocks whose source disappeared,
|
|
53
|
+
and never mirrors a block back to the calendar it came from.
|
|
54
|
+
|
|
55
|
+
## Getting started
|
|
56
|
+
|
|
57
|
+
### Prerequisites
|
|
58
|
+
|
|
59
|
+
- macOS
|
|
60
|
+
- every calendar you want to sync already subscribed in Calendar.app.
|
|
61
|
+
- Python 3.13 or later.
|
|
62
|
+
- Something to install Python packages: calque is a standard Python package — `uv`, `pipx` or `pip` all install it.
|
|
63
|
+
|
|
64
|
+
[`pipx`](https://pipx.pypa.io/) is recommended for a command-line tool, as it puts calque in its
|
|
65
|
+
own isolated environment and on your `PATH`:
|
|
66
|
+
|
|
67
|
+
```sh
|
|
68
|
+
pipx install calque
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
To install from a checkout of this repository, point any of them at the directory, e.g. `uv tool install .`
|
|
72
|
+
|
|
73
|
+
### Grant Access and View Calendars
|
|
74
|
+
|
|
75
|
+
EventKit is gated by macOS privacy controls. Run calque with `--list-calendars` once interactively: the first
|
|
76
|
+
run triggers a permission prompt; you grant access via a Ui dialog which is thereafter managed under
|
|
77
|
+
**System Settings → Privacy & Security → Calendars** for the app running calque.
|
|
78
|
+
In the first instance, this is your terminal. If you install calque as a `launchd` agent, the agent will not run under
|
|
79
|
+
the the terminal and will ask separately for permission.
|
|
80
|
+
Once granted, you can list every calendar's **account-qualified name** with:
|
|
81
|
+
|
|
82
|
+
```sh
|
|
83
|
+
calque --list-calendars
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The output is every calendar's **account-qualified name** (`Account.Calendar`). This is an example of the output you might see:
|
|
87
|
+
|
|
88
|
+
```text
|
|
89
|
+
Acme Consulting.Acme Consulting
|
|
90
|
+
Acme Consulting.Holidays in the United Kingdom
|
|
91
|
+
MoM.Calendar
|
|
92
|
+
MoSW.Birthdays
|
|
93
|
+
MoSW.Calendar
|
|
94
|
+
MoSW.United Kingdom holidays
|
|
95
|
+
Other.Birthdays
|
|
96
|
+
Subscribed Calendars.UK Holidays
|
|
97
|
+
iCloud.Home
|
|
98
|
+
iCloud.Personal
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
These qualified names are the form calque accepts: use them exactly as printed wherever a
|
|
102
|
+
calendar is named, quoting any that contain spaces.
|
|
103
|
+
|
|
104
|
+
### Dry Run
|
|
105
|
+
|
|
106
|
+
Always dry-run first: calque logs the exact plan (what it would create, update, and delete in each
|
|
107
|
+
calendar) and writes nothing. The first calendar is the primary; the rest are mirrored
|
|
108
|
+
against it.
|
|
109
|
+
|
|
110
|
+
```sh
|
|
111
|
+
calque "Acme Consulting.Acme Consulting" "MoSW.Calendar" "MoM.Calendar" "iCloud.Home" \
|
|
112
|
+
--title-to "Acme Consulting.Acme Consulting" "{account}: {title}" \
|
|
113
|
+
--title-from "iCloud.Home" "Busy" \
|
|
114
|
+
--mute "iCloud.Home" \
|
|
115
|
+
--dry-run
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
This is a typical consultant setup. `Acme Consulting.Acme Consulting` is your company calendar (the
|
|
119
|
+
hub), `MoSW.Calendar` and `MoM.Calendar` are two client tenants, and `iCloud.Home` is your personal
|
|
120
|
+
calendar. It mirrors all three into your company calendar, then fans your company calendar's combined availability back out to the two clients:
|
|
121
|
+
|
|
122
|
+
- `--title-to "Acme Consulting.Acme Consulting" "{account}: {title}"`: in your *own* company
|
|
123
|
+
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.
|
|
124
|
+
- `--title-from "iCloud.Home" "Busy"`: anything originating from your **personal** calendar is
|
|
125
|
+
labelled just `Busy` wherever it lands, so home detail never leaks — not even into your company
|
|
126
|
+
diary, which the `--title-to` rule would otherwise make detailed (a source override wins over a
|
|
127
|
+
target one).
|
|
128
|
+
- 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.
|
|
129
|
+
- `--mute "iCloud.Home"` — read your personal calendar as a source but never write blocks into it;
|
|
130
|
+
it stays untouched.
|
|
131
|
+
|
|
132
|
+
The two clients (`MoSW.Calendar`, `MoM.Calendar`) receive only the default opaque `Busy (…)` blocks,
|
|
133
|
+
so neither sees the other's events, your home detail, or anything beyond the fact that you're busy.
|
|
134
|
+
|
|
135
|
+
When the plan looks right, drop `--dry-run` to apply it once, or install it as an agent to keep both
|
|
136
|
+
sides in sync (next step).
|
|
137
|
+
|
|
138
|
+
### Install Agent
|
|
139
|
+
|
|
140
|
+
Schedule calque to run automatically with `launchd`. calque installs its own lanunchd agent:
|
|
141
|
+
pass `--install SECONDS` with the same arguments you previously (dry-)ran, and it writes and loads the
|
|
142
|
+
agent for you (no plist to edit):
|
|
143
|
+
|
|
144
|
+
When the agent first runs from launchd, it will prompt for calendar access via a system dialog.
|
|
145
|
+
This will show up as `python3.x` this time, instead of terminal.
|
|
146
|
+
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.
|
|
147
|
+
|
|
148
|
+
```sh
|
|
149
|
+
calque "Acme Consulting.Acme Consulting" "MoSW.Calendar" "MoM.Calendar" iCloud.Home \
|
|
150
|
+
--title-to "Acme Consulting.Acme Consulting" "{account}: {title}" \
|
|
151
|
+
--title-from "iCloud.Home" "Busy" \
|
|
152
|
+
--mute "iCloud.Home" \
|
|
153
|
+
--install 180 # run every 3 minutes
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Remove it with:
|
|
157
|
+
|
|
158
|
+
```sh
|
|
159
|
+
calque --uninstall
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Options
|
|
163
|
+
|
|
164
|
+
- **`calendars`** (positional, two or more) — the calendars to sync, each a qualified name from
|
|
165
|
+
`--list-calendars`. The **first** is your primary calendar: every other is mirrored into it,
|
|
166
|
+
and it is then mirrored back out to each.
|
|
167
|
+
- **`--title TEMPLATE`** — default title template for every mirror block (default
|
|
168
|
+
`Busy ({account} calendar)`). `{field}` placeholders are filled from the source event; see
|
|
169
|
+
[Titles](#titles).
|
|
170
|
+
- **`--title-to NAME TEMPLATE`** — title template used when writing **into** NAME's calendar.
|
|
171
|
+
Repeatable.
|
|
172
|
+
- **`--title-from NAME TEMPLATE`** — title template for events read **from** NAME's calendar,
|
|
173
|
+
wherever they land; **wins over** `--title-to`. Repeatable.
|
|
174
|
+
- **`--mute NAME …`** — calendars to read as a source but never write blocks into.
|
|
175
|
+
- **`--lookback DAYS`** — days before now to keep mirrored (default `1`). With `--cleanup`, this
|
|
176
|
+
instead bounds the window within which finished blocks are removed.
|
|
177
|
+
- **`--lookahead DAYS`** — days after now to mirror (default `60`).
|
|
178
|
+
- **`--cleanup`** / **`--no-cleanup`** — remove a mirror block once its event is over, instead of
|
|
179
|
+
keeping it for the lookback window (default off).
|
|
180
|
+
- **`--exclude-pattern REGEX …`** — replace the default title-exclusion patterns (see
|
|
181
|
+
[Exclusions](#exclusions)).
|
|
182
|
+
- **`--exclude-clashes`** / **`--no-exclude-clashes`** — skip a source event that overlaps a genuine
|
|
183
|
+
event already on the target (default on).
|
|
184
|
+
- **`--exclude-all-day`** / **`--no-exclude-all-day`** — skip all-day events (default on).
|
|
185
|
+
- **`--exclude-out-of-hours`** / **`--no-exclude-out-of-hours`** — skip events that fall entirely
|
|
186
|
+
outside working hours (default on).
|
|
187
|
+
- **`--dry-run`** — log the plan without writing any changes.
|
|
188
|
+
- **`--list-calendars`** — print every calendar's account-qualified name and exit.
|
|
189
|
+
- **`--install SECONDS`** — install a `launchd` agent that runs this same command every SECONDS,
|
|
190
|
+
then exit.
|
|
191
|
+
- **`--uninstall`** — remove the installed `launchd` agent and exit.
|
|
192
|
+
- **`--logging LEVEL`** — logging level (default `info`; `debug` shows why each event was kept or
|
|
193
|
+
excluded).
|
|
194
|
+
Be careful not to place variadic options (`--mute`, `--exclude-pattern`) immediately before the calendar arguments,
|
|
195
|
+
or they will swallow them.
|
|
196
|
+
|
|
197
|
+
## Titles
|
|
198
|
+
|
|
199
|
+
The mirror-block title is a template. Placeholders are filled from the source event,
|
|
200
|
+
so `{account}` is the source account and `{title}` is the event's real subject. The default,
|
|
201
|
+
`Busy ({account} calendar)`, doesn't include `{title}` and so keeps the details opaque.
|
|
202
|
+
{account} is always the source of the current event, so when mirroring events from the primary calendar
|
|
203
|
+
into a client calendar, {account} is the primary calendar's account (even if the busy block originated
|
|
204
|
+
from a different client calendar). This ensures that, if using `{account}`, auxiliary calendars never see the
|
|
205
|
+
names of calendars other than the primary (which we assume they already know).
|
|
206
|
+
|
|
207
|
+
In addition to `--title`, two overrides control the title used for a calendar, both repeatable and
|
|
208
|
+
both keyed on a calendar's qualified name:
|
|
209
|
+
|
|
210
|
+
- `--title-to NAME TEMPLATE` — the title used when writing *into* NAME's calendar ("when putting
|
|
211
|
+
events into *this* diary, use this format").
|
|
212
|
+
- `--title-from NAME TEMPLATE` — the title used for events read *from* NAME's calendar, wherever
|
|
213
|
+
they land. A title-from override wins over a title-to one.
|
|
214
|
+
|
|
215
|
+
Common use-cases:
|
|
216
|
+
|
|
217
|
+
`--title-to` lets your *own* diary show real subjects while every client calendar still sees only
|
|
218
|
+
opaque busy blocks (and so never sees detail of your own or other clients' events):
|
|
219
|
+
|
|
220
|
+
`--title-from` pins how one source always appears, regardless of the target. For example, feed a
|
|
221
|
+
personal calendar into the company diary to block out home commitments, but keep those entries
|
|
222
|
+
fully opaque even if a `--title-to` on that calendar would otherwise make them detailed:
|
|
223
|
+
|
|
224
|
+
```sh
|
|
225
|
+
calque "Acme Consulting.Acme Consulting" "MoSW.Calendar" "MoM.Calendar" "iCloud.Home" \
|
|
226
|
+
--title-to "Acme Consulting.Acme Consulting" "{account}: {title}" \
|
|
227
|
+
--title-from "iCloud.Home" "Busy"
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## Exclusions
|
|
231
|
+
|
|
232
|
+
Before mirroring, every source event runs through a set of **exclusion rules**; if any rule
|
|
233
|
+
rejects it, no busy block is written. The rules are assembled per run in
|
|
234
|
+
[`exclusions.py`](src/calque/exclusions.py), so adding a new kind is a matter of writing one
|
|
235
|
+
builder and wiring it into `rules`.
|
|
236
|
+
|
|
237
|
+
Current rules:
|
|
238
|
+
|
|
239
|
+
- **By status** — only events with a busy status are mirrored. The default busy status is only
|
|
240
|
+
`accepted`, but you can override them on `Config`.
|
|
241
|
+
- **By title** — source events whose title matches any `--exclude-pattern` regular expression
|
|
242
|
+
are skipped, so availability markers don't get mirrored as meetings. The default excludes a
|
|
243
|
+
bare `Working` status block (`^Working$`) and any annual-leave marker (`\bA/L\b`). Pass one
|
|
244
|
+
or more of your own patterns to replace the defaults.
|
|
245
|
+
- **All-day** — all-day events (out-of-office banners, leave, public holidays) are skipped so
|
|
246
|
+
they don't blank out the whole day on the other calendar. On by default; `--no-exclude-all-day`.
|
|
247
|
+
- **Out of hours** — events that fall entirely outside working hours are skipped, so only your
|
|
248
|
+
working day is mirrored. The window defaults to **Monday–Friday 08:00–18:00 local time**; an
|
|
249
|
+
event that overlaps it at all (e.g. a 17:00–19:00 overrun) is still mirrored. On by default;
|
|
250
|
+
`--no-exclude-out-of-hours`. The window itself (`work_days`, `work_start`, `work_end`) is set
|
|
251
|
+
on `Config`.
|
|
252
|
+
- **By clash** — a source event is skipped when the target calendar already has a genuine event
|
|
253
|
+
(not one of calque's own mirror blocks) overlapping any part of its slot, so calque never
|
|
254
|
+
stacks a busy block on time you're already committed elsewhere. On by default; turn it off
|
|
255
|
+
with `--no-exclude-clashes`.
|
|
256
|
+
|
|
257
|
+
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:
|
|
258
|
+
```sh
|
|
259
|
+
calque "Acme Consulting.Acme Consulting" "MoSW.Calendar" \
|
|
260
|
+
--exclude-pattern "^Working$" "\bA/L\b" "\bLunch " --no-exclude-clashes
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
## Writing into every calendar
|
|
264
|
+
|
|
265
|
+
Availability is mirrored in both directions by default for every calendar you list.
|
|
266
|
+
If you want to read a calendar but not write into it, pass `--mute NAME` for that calendar.
|
|
267
|
+
Note that if you mute the primary calendar, it will mirror its events into the other calendars,
|
|
268
|
+
but will not not be able to mirror busy blocked between the other calendars - i.e. you have muted
|
|
269
|
+
the hub so it can't act as a hub for mirroring.
|
|
270
|
+
|
|
271
|
+
## Status filtering
|
|
272
|
+
|
|
273
|
+
Your response is read from the event's attendee list.
|
|
274
|
+
Only events you've accepted are mirrored by default (the busy statuses are configurable on
|
|
275
|
+
`Config`). Events with no attendees (blocks you created yourself) are treated as implicitly accepted.
|