Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions .github/workflows/linkcheck.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
name: Link check

on:
push:
branches:
- master
pull_request:
workflow_dispatch:
schedule:
- cron: "03 22 * * *"

concurrency:
group: linkcheck-${{ github.ref }}
cancel-in-progress: false

jobs:
linkcheck:
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- uses: actions/checkout@v5
- name: Restore lychee cache
uses: actions/cache@v4
with:
path: .lycheecache
key: cache-lychee-${{ github.run_id }}
restore-keys: cache-lychee-
- name: Check links with Lychee
id: lychee
uses: lycheeverse/lychee-action@v2
with:
fail: false
args: >-
--timeout 30
--max-retries 6
--retry-wait-time 2
--cache
--max-cache-age 14d
.
- name: Create or update Link Checker issue
if: steps.lychee.outputs.exit_code != 0 && github.ref == 'refs/heads/master'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Keep the oldest open report issue as canonical, fold duplicates in.
ISSUES=$(gh issue list --label "report" --state open --json number --jq '.[].number' | sort -n)
CANON=$(printf '%s\n' "$ISSUES" | head -1)
for n in $(printf '%s\n' "$ISSUES" | tail -n +2); do
gh issue close "$n" --comment "Duplicate of #${CANON} - auto-closed by the link checker."
done
if [ -n "$CANON" ]; then
gh issue edit "$CANON" --body-file ./lychee/out.md
else
gh issue create --title "Link Checker Report" --body-file ./lychee/out.md --label "report"
fi
- name: Close Link Checker issue if all links are healthy
if: steps.lychee.outputs.exit_code == 0 && github.ref == 'refs/heads/master'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
for n in $(gh issue list --label "report" --state open --json number --jq '.[].number'); do
gh issue close "$n" --comment "All links are now healthy."
done
37 changes: 37 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: Publish to PyPI

on:
push:
tags:
- "v*"

jobs:
publish:
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/p/calendar-cli
permissions:
id-token: write

steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0

- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.12"

- name: Install build tools
run: pip install build

- name: Build package
run: python -m build

- name: Verify built version matches the tag
run: ls -lh dist/

- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
33 changes: 33 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: Tests

on:
push:
branches: [master]
pull_request:

jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]

steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}

- name: Install dependencies
run: pip install -e ".[dev]"

- name: Run ruff
run: ruff check .

- name: Run tests
run: pytest
16 changes: 16 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Version file generated at build time by hatch-vcs
calendar_cli/_version.py

# Editor backups / autosave files
*~
\#*\#
.\#*
*.orig
*.bak

# Python build artifacts
build/
dist/
*.egg-info/
__pycache__/
*.py[cod]
57 changes: 57 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
repos:
# === ai-prompt-auto-commit: record Claude Code prompts in commit messages ===
- repo: https://github.com/pycalendar/ai-prompt-auto-commit
rev: v0.0.5
hooks:
Expand All @@ -9,3 +10,59 @@ repos:
stages: [post-commit]
- id: prepare-ai-repository
stages: [manual]

# === PRE-COMMIT STAGE (quick checks on every commit) ===
# NOTE: ruff-format is intentionally omitted. calendar_cli is a legacy/LTS
# codebase and we do not want a wholesale reformat; ruff lint (scoped to real
# errors in pyproject.toml) is enough.
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.19
hooks:
- id: ruff-check
args: [--fix]

- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files

# === COMMIT-MSG: Enforce Conventional Commits ===
- repo: https://github.com/compilerla/conventional-pre-commit
rev: v4.4.0
hooks:
- id: conventional-pre-commit
stages: [commit-msg]

# === PRE-PUSH: full test suite before pushing ===
- repo: local
hooks:
- id: pytest
name: pytest
entry: pytest
language: system
pass_filenames: false
always_run: true
stages: [pre-push]

# === PRE-PUSH: prevent direct pushes to master (mature, v1+ project) ===
# Bypass with: git push --no-verify
- repo: local
hooks:
- id: no-push-to-main
name: Prevent direct push to master branch
entry: bash -c 'branch=$(git rev-parse --abbrev-ref HEAD); if [ "$branch" = "main" ] || [ "$branch" = "master" ]; then echo "Direct pushes to $branch are not allowed. Use --no-verify to bypass."; exit 1; fi'
language: system
pass_filenames: false
stages: [pre-push]

# === PRE-PUSH: link checker (cached for speed; config in lychee.toml) ===
- repo: https://github.com/lycheeverse/lychee
rev: lychee-v0.24.2
hooks:
- id: lychee
args: ["--no-progress", "--timeout", "10", "--cache", "--max-cache-age", "1d", "."]
pass_filenames: false
stages: [pre-push]
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Changed
- Packaging migrated from `setup.py` to a PEP 621 `pyproject.toml` using the
Hatch build backend. The version is now derived automatically from git tags
via hatch-vcs instead of being hard-coded in `calendar_cli/metadata.py`, so
the released version can no longer drift from the tags
(https://github.com/pycalendar/calendar-cli/issues/117).

### Fixed
- `--interactive-config` no longer crashes on Python 3: it relied on the
removed Python 2 `raw_input` builtin and used `os`, `time` and `getpass`
without importing them.
- The package license metadata now uses the valid SPDX identifier
`GPL-3.0-or-later`
(https://github.com/pycalendar/calendar-cli/issues/115).
56 changes: 56 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Contributing to calendar-cli

Contributions are mostly welcome (but do inform about it if you've used AI or
other tools). If the length of this text scares you, then I'd rather want you
to skip reading and just produce a pull-request in GitHub. If you find it too
difficult to write test code, etc, then you may skip it and hope the maintainer
will fix it.

## What to include

Every submission should ideally include:

- **Test code** covering the new behaviour or bug fix
- **Documentation** updates where relevant
- **A changelog entry** in `CHANGELOG.md` under `[Unreleased]`

## Development setup

```
make dev # editable install with dev dependencies (pytest, ruff, pre-commit)
make test # run the test suite
make lint # run ruff
```

To enable the pre-commit hooks (linting on commit, tests/link-check on push,
conventional-commit message checks):

```
pre-commit install
pre-commit install --hook-type pre-push
pre-commit install --hook-type commit-msg
```

## Versioning and releases

The version is derived automatically from git tags via hatch-vcs — it is **not**
stored in the source. A release is made by tagging (`vX.Y.Z`) and pushing the
tag; a GitHub Actions workflow then builds and publishes to PyPI via trusted
publishing. There is nothing to bump by hand.

## Commit messages

Please follow [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/)
and write messages in the imperative mood:

- `fix: correct time-range search handling for recurring events`
- `feat: add new command`
- `docs: update README`

Rather than:

- `This commit fixes the time-range search`
- `Added new command`

Note: older commits in this repository predate this convention and do not
follow it.
46 changes: 46 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
.PHONY: help install dev lint test clean venv-install

help: ## Show this help
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | \
awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-15s\033[0m %s\n", $$1, $$2}'

install: ## Install the package (auto-detects root, uv, pipx, or pip)
@if [ "$$(id -u)" = "0" ]; then \
echo "Running as root, installing system-wide..."; \
pip install .; \
elif command -v uv >/dev/null 2>&1; then \
echo "Installing with uv..."; \
uv tool install .; \
elif command -v pipx >/dev/null 2>&1; then \
echo "Installing with pipx..."; \
pipx install .; \
else \
echo "Tip: Install uv or pipx for isolated installs (pacman -S uv, apt install pipx, brew install uv)"; \
echo "Falling back to pip install --user ..."; \
PIP_BREAK_SYSTEM_PACKAGES=1 pip install --user .; \
fi

dev: ## Install with dev dependencies (editable)
PIP_BREAK_SYSTEM_PACKAGES=1 pip install -e ".[dev]"

lint: ## Run ruff linter
python -m ruff check calendar_cli/ tests/

test: ## Run tests
python -m pytest

clean: ## Remove build artifacts
rm -rf dist/ build/ *.egg-info calendar_cli/_version.py .pytest_cache .ruff_cache
find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true

VENV := .venv
WRAPPER_DIR := $(HOME)/bin

venv-install: ## Install into a venv and create a ~/bin/calendar-cli wrapper
python3 -m venv $(VENV)
$(VENV)/bin/pip install --upgrade pip
$(VENV)/bin/pip install .
mkdir -p $(WRAPPER_DIR)
@printf '#!/bin/sh\nexec %s/bin/calendar-cli "$$@"\n' "$$(pwd)/$(VENV)" > $(WRAPPER_DIR)/calendar-cli
chmod +x $(WRAPPER_DIR)/calendar-cli
@echo "Installed: $(WRAPPER_DIR)/calendar-cli -> $$(pwd)/$(VENV)/bin/calendar-cli"
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,9 @@ Installation
------------

`calendar-cli` depends on quite some python libraries, i.e. pytz, caldav, etc.
"sudo ./setup.py install" should take care of all those eventually, and will
also make an executable under /usr/bin
Run `make install` — it auto-detects `uv`, `pipx` or `pip` and installs the
package together with all its dependencies, exposing a `calendar-cli`
executable. Dependencies are declared in `pyproject.toml`.

Support
-------
Expand Down
Loading
Loading