A Python Flask web application for bulk Google Calendar event creation using natural language processing. Create multiple events at once by typing them in plain English, with support for routing events to different calendars using simple aliases.
- Bulk Event Creation: Add multiple events at once, one per line
- Natural Language Processing: Use Google's quickAdd API to parse event text like "Dinner with Sarah at 7pm tomorrow"
- Calendar Aliases: Route events to specific calendars using
@aliassyntax (e.g.,@workout Push day Monday 7pm) - Web Interface: Clean, modern UI for managing events and aliases
- Flask Blueprints: Modular architecture for events, auth, and settings
- OAuth Authentication: Secure Google Calendar API integration
- Python 3.14+
- uv package manager
- Google Cloud Console project with Calendar API enabled
-
Install uv:
curl -LsSf https://astral.sh/uv/install.sh | sh -
Install Python 3.14:
uv python install 3.14
-
Clone and setup:
git clone https://github.com/simonteague6/Batch-Add-Events cd bulk-calendar-app uv sync -
Configure Google OAuth:
- Go to Google Cloud Console
- Create a project and enable Google Calendar API
- Create OAuth 2.0 credentials (Desktop app)
- Download
credentials.jsonand place indata/directory - Add
http://localhost:5485/oauth2callbackto authorized redirect URIs
-
Run the application:
uv run run.py
-
First-time setup:
- Visit
http://localhost:5485 - Authenticate with Google when prompted
- Go to Settings (gear icon) to configure your calendar aliases
- Visit
-
Build the Docker image:
docker compose build
-
Configure Google OAuth:
- Go to Google Cloud Console
- Create a project and enable Google Calendar API
- Create OAuth 2.0 credentials (Desktop app)
- Download
credentials.jsonand place indata/directory - Add
http://localhost:5485/oauth2callbackto authorized redirect URIs
-
Start the container:
docker compose up
-
Create Events:
- Visit
http://localhost:5485 - Type events, one per line:
@workout Push day Monday 7pm @work Team meeting Friday 2pm Dinner with Sarah at 7pm tomorrow - Events with
@aliasgo to that calendar, others go to primary
- Visit
-
Configure Aliases:
- Click the settings gear icon
- Enter short names next to calendars you can write to
- Read-only calendars are hidden by default (click to expand)
- Save your changes
-
Using Aliases:
- Type
@aliasbefore events to route to specific calendars - Use
primaryfor your main calendar (or leave alias blank) - Example:
@workout Push day Monday 7pm
- Type
uv run -m app.services.list_calendarsLists all calendars with their IDs (useful for debugging).
Aliases are stored in data/calendar_aliases.json:
{
"workout": "abc123@group.calendar.google.com",
"work": "xyz789@group.calendar.google.com",
"personal": "primary"
}- Keys are the aliases you type (without
@) - Values are the actual Google Calendar IDs
- Use
primaryfor your main calendar
FLASK_ENV: Set toproductionto disable debug modeSECRET_KEY: Flask session secret (defaults to dev key)
.
├── run.py # Application entry point
├── pyproject.toml # Project dependencies
├── Dockerfile # Container configuration
├── docker-compose.yml # Docker orchestration
├── data/
│ └── calendar_aliases.json # User's alias configuration
└── app/ # Flask application
├── __init__.py # Flask app factory
├── services/ # Core services
│ ├── calendar_client.py # Google Calendar API client
│ ├── alias_parser.py # @alias text parser
│ └── list_calendars.py # List all calendar IDs
├── events/ # Event creation blueprint
│ ├── routes.py # Event-related routes
│ └── templates/
│ └── events/
│ └── index.html # Main event creation page
├── auth/ # Authentication blueprint
│ ├── routes.py # OAuth routes
│ └── __init__.py
└── settings/ # Settings blueprint
├── routes.py # Alias management routes
└── __init__.py
# Check code
uvx ruff check .
# Format code
uvx ruff format .# Run all tests
uv run pytest
# Run with verbose output
uv run pytest -v
# Run with coverage report
uv run pytest --cov=app --cov-report=term-missing
# Run specific test file
uv run pytest tests/test_alias_parser.pyThe test suite uses pytest with mocked Google Calendar API calls. Tests cover:
- Alias parsing logic (
tests/test_alias_parser.py) - Calendar client functions (
tests/test_calendar_client.py) - Route handlers (
tests/test_routes.py) - Calendar listing (
tests/test_list_calendars.py)
The interface adapts to different screen sizes:
- Desktop: Full-width layouts with side-by-side elements
- Tablet: Adjusted spacing and scaled typography
- Mobile: Stacked layouts, larger touch targets (44px minimum), and optimized form inputs
- Typing events: The textarea expands as you type. Use one line per event.
- Adding aliases: Type
@aliasfollowed by a space before your event text (e.g.,@work Meeting at 2pm) - Settings: Read-only calendars are hidden by default—tap "Show read-only calendars" to view them
- Touch targets: All buttons and links are sized for easy tapping
- Theme: Toggle between light and dark mode using the button in the navigation bar
- Event Parsing: When you submit events,
app/services/alias_parser.pyextracts@aliasprefixes using regex - Alias Lookup: The alias is looked up in
data/calendar_aliases.jsonto get the real calendar ID - API Call: Google Calendar API's
quickAddendpoint parses the natural language and creates the event - Error Handling: Unknown aliases show error messages; API errors are caught and displayed
- Never commit
credentials.jsonortoken.json(they're in .gitignore) token.jsonstores your OAuth tokens locally- OAuth scope is limited to
https://www.googleapis.com/auth/calendar - Use environment variables for production secrets
"Unknown calendar alias" error:
- Check your alias configuration in Settings
- Verify the alias exists in
data/calendar_aliases.json
Authentication errors:
- Delete
token.jsonto re-authenticate - Ensure
credentials.jsonis valid and not expired
Events not appearing:
- Check the correct calendar is selected in Google Calendar UI
- Verify the calendar ID in your alias configuration
Mobile issues:
- If text is too small, use browser zoom or check device accessibility settings
- Clear browser cache if the app seems stuck or unresponsive