notion-calendar-generator 0.1.0
Generate hierarchical calendar pages in Notion (Year > Quarter > Month > Week > Day) with optional recurring tasks.
License: MIT
Requires Python: <4.0,>=3.9
Keywords: notion, calendar, productivity, automation

# Notion Calendar Generator

Generate a full hierarchical calendar in Notion with interconnected databases: **Year > Quarters > Months > Weeks > Days**.

## Installation

```bash
pip install notion-calendar-generator
```

## Setup

### 1. Create a Notion integration

Create an integration at [notion.so/my-integrations](https://www.notion.so/my-integrations) and copy the API key.

### 2. Create Notion databases

Create five databases in Notion (yearly, quarterly, monthly, weekly, daily) and share them with your integration. Each database needs at minimum:

- A **title** property
- A **Date** property
- **Relation** properties linking to parent databases:
  - Quarterly → `🕯 annual-data` (relates to yearly)
  - Monthly → `🕯 annual-data` + `❄ quarterly-data`
  - Weekly → `🌘 monthly-data`
  - Daily → `🏁 weekly-data`

### 3. Configure environment

Create a `.env` file:

```
NOTION_KEY=secret_xxx
YEARLY_DATA_ID=xxx
QUARTERLY_DATA_ID=xxx
MONTHLY_DATA_ID=xxx
WEEKLY_DATA_ID=xxx
DAILY_DATA_ID=xxx
STEPS_DATA_ID=xxx
```

For test databases, use the same keys prefixed with `TEST_` (e.g. `TEST_NOTION_KEY`).

`STEPS_DATA_ID` is only required if you use the `--tasks` feature.

## Usage

```bash
# Generate calendar for a year
nocage 2025

# Use a custom .env path
nocage 2025 --env-path /path/to/.env

# Run against test databases
nocage 2025 --test
```

## Caching

Page IDs are cached locally in `.nocage/cache.db` (SQLite) to make operations **idempotent** -- running the same year twice won't create duplicates.

### Stale cache handling

If pages were deleted from Notion but still exist in cache:

```bash
# Default: error and exit with a message
nocage 2025

# Delete stale entries from cache and recreate pages
nocage 2025 --refresh-cache

# Recreate missing pages in Notion and update cache
nocage 2025 --refresh-notion

# Skip caching entirely (original behavior, will create duplicates)
nocage 2025 --no-cache
```

## Generated structure

For year 2025, the tool creates:

| Type    | Count   | Name format | Example      |
| ------- | ------- | ----------- | ------------ |
| Year    | 1       | `YYYY`      | `2025`       |
| Quarter | 4       | `YY-QN`     | `25-Q1`      |
| Month   | 12      | `YY-Month`  | `25-January` |
| Week    | ~52     | `YY-week-N` | `25-week-1`  |
| Day     | 365/366 | `YY-day-N`  | `25-day-1`   |

## Recurring tasks

Generate recurring task pages in Notion's **steps** database, with relations to pages in **trees**, **seas**, and/or **hikes** databases.

### Setup

1. Create a **steps** database in Notion with:
   - A **title** property
   - A **Date** property
   - Relation properties: `🌲 trees`, `🌊 seas`, `🧗 hike`
2. Add `STEPS_DATA_ID` to your `.env`

### Task config (`tasks.yaml`)

The config has two sections: **aliases** (friendly names for Notion page IDs) and **tasks** (what to generate).

```yaml
aliases:
  trees:
    programming: "notion-page-id-for-programming-tree"
    fitness: "notion-page-id-for-fitness-tree"
  seas:
    work: "notion-page-id-for-work-sea"
  hikes:
    wellness: "notion-page-id-for-wellness-hike"
```

Under `relations` in each task, reference aliases by name instead of raw IDs. Relations are optional and variable — a task can relate to any combination of trees, seas, and hikes.

### Frequency types

There are four frequency types. Each task uses exactly one.

#### 1. Interval: `every` + `unit`

Repeat every N days, weeks, months, quarters, or years. Use `start_date` to control when the first occurrence lands (defaults to Jan 1).

```yaml
# Every 2 days starting Jan 1
- name: "Clean cat litter"
  frequency:
    every: 2
    unit: days
    start_date: 2025-01-01

# Every 2 weeks starting Jan 6 (a Monday)
- name: "Biweekly retro"
  frequency:
    every: 2
    unit: weeks
    start_date: 2025-01-06

# Monthly on the 15th
- name: "Monthly review"
  frequency:
    every: 1
    unit: months
    start_date: 2025-01-15

# Quarterly on the 1st
- name: "Quarterly goals"
  frequency:
    every: 1
    unit: quarters
    start_date: 2025-01-01
```

For monthly/quarterly frequencies, the day from `start_date` is preserved. Days that don't exist in a month are clamped (e.g. the 31st becomes the 28th in February).

#### 2. Days of week: `days_of_week`

Repeat on specific weekdays, every week.

```yaml
# Every Monday and Thursday
- name: "Weekly standup"
  frequency:
    days_of_week: [monday, thursday]

# Every Saturday
- name: "Weekly reset"
  frequency:
    days_of_week: [saturday]
```

#### 3. Even/odd day of month: `day_of_month`

Repeat on every even or odd day-of-month (2nd, 4th, 6th... or 1st, 3rd, 5th...).

```yaml
# Every even day (2nd, 4th, 6th, 8th, ... 28th, 30th)
- name: "Alom pucolas"
  frequency:
    day_of_month: even
```

#### 4. Nth weekday of month: `weekday_of_month`

Repeat on a specific weekday position within a month. Optionally restrict to specific months.

Positions: `first`, `second`, `third`, `fourth`, `last`.

```yaml
# Last Saturday of every month
- name: "Monthly reset"
  frequency:
    weekday_of_month:
      position: last
      day: saturday

# First Saturday of each quarter (Jan, Apr, Jul, Oct)
- name: "Quarterly reset"
  frequency:
    weekday_of_month:
      position: first
      day: saturday
      months: [january, april, july, october]

# Second Wednesday of every month
- name: "Book club"
  frequency:
    weekday_of_month:
      position: second
      day: wednesday
```

### Running

```bash
# Generate calendar + recurring tasks
nocage 2025 --tasks tasks.yaml

# With caching options
nocage 2025 --tasks tasks.yaml --refresh-cache
```

Each step is created as `"Task name - YYYY-MM-DD"` in the steps database, with its Date property set and relations linked.

## Development

### Running tests

```bash
poetry install
poetry run pytest tests/ --cov=notion_calendar_generator
```

### Releasing a new version

```bash
# 1. Bump version (patch/minor/major)
poetry version minor

# 2. Build
poetry build

# 3. Test on TestPyPI first
poetry publish -r testpypi

# 4. Publish to PyPI
poetry publish
```