CLI tool for managing Lähellä.fi activity listings via YAML
Project description
lahella-cli: Lähellä.fi Activity Automation
Automate creating and updating activity listings on Lähellä.fi using YAML configuration files.
What is Lähellä.fi?
Finnish authorities, in their great but subinfinite wisdom, created the Palvelutietovaranto (PTV), a glorious incarnation of the Semantic Web for Finnish services. State and council services have to be listed in it by law, but it is also open to private entities such as sports associations and hobby clubs. The upside of listing your services there is that it will show up in all sorts of official searches. For example, I volunteer with a tai chi association and have found that it is great for developing my sense of balance, and perhaps one day somewhere a healthcare worker is looking for a recommendation for exercises for a patient who has balance problems, and being legible in PTV would help us be findable at that moment.
Look, it's more European than Google! Also, at Google's rate of enshittification even PTV may overtake it in usefulness before the heat death of the universe.
In any case, implementing PTV is an exercise in bureaucracy so spectacularly byzantine that it has to be seen to be believed. Thus Lähellä.fi: a way for the small-time sports and hobby clubs to list their services. Lähellä.fi synchronises with PTV automatically and reduces the complexity to perhaps one tenth.
Alas, even one deci-PTV of complexity is still way too clunky for us mere mortals. We have several tai chi classes and exercise groups, and keeping the listings up to date is a chore that you have to do through a next.js interface that is... let's say "not bad for a government project". Thus this automation tool.
Who is this for?
You need to have a lähellä.fi account and some computing experience. I mean, you have to be able to deal with YAML files and appreciate why it's a better experience than clicky-clicky web forms.
It's probably worthwhile if you want to maintain at least five different activity entries (anything less and you're complicating your life for very little gains). What you get:
- Batch Management - Create or update all activities at once instead of filling forms one-by-one
- Version Control - Track your activity listings in Git, see what changed over time, and collaborate with teammates
- Automation - Eliminate repetitive clicking through the web interface
- Consistency - Define templates for common patterns (locations, schedules, pricing) and reuse them across activities
Caveats
This is version 0.1, mainly vibe-coded, it worked for me once! Please take backups of your lähellä.fi data before using this alpha software.
Installation
-
Install uv - I mean, everyone has done this in 2025, but see instructions at https://docs.astral.sh/uv/getting-started/installation/
-
Clone or download this repository:
git clone https://github.com/jkseppan/lahella-cli.git cd lahella-cli
-
Install dependencies:
uv sync
Quick Start
1. Set Up Authentication
Create an auth.yaml file with your Lähellä.fi credentials:
auth:
email: your.email@example.com
password: your_password
group_id: "your_organization_group_id"
cookies: ""
Finding your group ID:
- Log in to hallinta.lahella.fi
- Go to https://hallinta.lahella.fi/en-GB/groups
Run the login script to authenticate and save session tokens:
uv run login.py
This uses automated browser login (via Playwright) to obtain authentication tokens and saves them to auth.yaml.
If that fails (because of Captcha or something) try getting the auth cookies by hand from a browser.
2. Create Your First Activity
Go clickety-click in the interface to create an activity. You'll see what the fields mean and what they want you to do with them.
3. Download the activity
uv run download_activities.py --yaml --output events.yaml
Understanding the YAML format:
- Text fields like
summaryanddescriptionuse HTML formatting with<p dir="ltr">for paragraphs (lest you forget which way the three supported languages Finnish, Swedish and English are written)- Perhaps write a fairly complete text in the web interface first, then edit the downloaded YAML later
weekdayuses numbers: 1=Monday, 7=Sunday- Dates are in YYYY-MM-DD format
4. Create more activities
Edit the YAML file and add another activity! Then try creating it remotely:
uv run create_course.py
The script will show a list of locally defined activities and tell you how to pick the one to create.
5. Download again
uv run download_activities.py --yaml --output downloaded_events.yaml
Compare to your original file.
There should be _id and _status fields for your new event.
The script will not automatically publish the event; for now,
you'll have to do it via the web interface.
There are likely some other changes, such as coordinates added to addresses.
If the file looks good, move it in place:
mv downloaded_events.yaml events.yaml
6. Edit and Sync
Edit the activities, see how your local and remote versions differ:
uv run sync_activities.py --all
and if you want to commit the changes to the remote:
uv run sync_activities.py --all --apply
7. Set up YAML anchors and aliases
The point of this whole exercise is to reduce duplicated effort, and the reason to tolerate YAML is to be able to use its features. To wit, anchors and aliases:
defaults: # define data here for use in multiple places
location: &common_location
type: place
accessibility: [ac_unknow]
regions: [city/FI/Helsinki]
address:
city: Helsinki
state: Uusimaa
country: FI
events: # the API calls will be made based on this part
- title:
fi: Morning Class
location:
<<: *common_location # add or override the rest of the attributes below:
address:
street: Street A 1
postal_code: "00100"
# ... rest of activity ...
- title:
fi: Evening Class
location:
<<: *common_location
address:
street: Street B 2
postal_code: "00200"
# ... rest of activity ...
The <<: *common_location syntax includes all fields from the template, and you only need to override what's different.
Unless you really love YAML, just use an LLM to find the shared similarities and
generate anchors in the defaults section.
Now we should remember that the YAML is all generated client-side,
so downloading a fresh copy of events.yaml
will cheerfully overwrite all your neat anchor-based deduplication.
The solution is to not overwrite it but write a separate file
and let the download script reference the original.
(It automatically looks for events.yaml but you can use -t something_else.yaml.)
It should detect when to use the <<: * operator.
Authentication Management
Token Refresh
The tool automatically refreshes authentication tokens when they expire. If you see "Unauthorized" errors:
uv run auth_helper.py
This attempts to refresh your tokens. If refresh fails, run uv run login.py again to re-authenticate.
Configuration Reference
This is all reverse-engineered from the API (because who provides documentation?), so caveat emptor and bring your own debugger.
Activity Fields
| Field | Required | Description |
|---|---|---|
title |
Yes | Activity name (multilingual: {fi: "...", sv: "...", en: "..."}) |
summary |
Yes | Brief description in HTML format |
description |
Yes | Detailed description in HTML format |
type |
Yes | Activity type: hobby, support, or voluntary |
required_locales |
Yes | Languages provided (e.g., [fi, sv, en]) |
categories |
Yes | Themes, formats, and locales (see below) |
demographics |
No | Target age groups and gender |
pricing |
Yes | type: free or paid, optional info text |
location |
Yes | Where the activity happens |
schedule |
Yes | When the activity happens |
registration |
No | How to register |
contacts |
No | Contact information |
image |
No | Photo for the activity listing |
Valid Category Values
Types:
hobby- Hobbies & leisuresupport- Support & assistancevoluntary- Voluntary work
Themes (for hobby type):
ht_digi_teknologia- Tech & gaminght_hyvinvointi- Wellness & lifestyleht_kansainvalisyys- International activitiesht_kulttuuri- Culture & artsht_kadentaidot- Crafts & handicraftsht_luonto- Nature & animalsht_maanpuolustus- National defenseht_pelastustoiminta- Fire & rescue servicesht_urheilu- Sports & exerciseht_uskonnot- Religion & spiritualityht_vaikuttaminen- Advocacy, democracy & human rights
Formats (for hobby type):
hm_esitykset- Performances & showshm_harrastukset- Hobbies & activitieshm_kohtaamispaikka- Meeting places & community spaceshm_kylatoiminta- Village & neighborhood activitieshm_leirit- Camps, trips & excursionshm_nayttelyt- Exhibitionshm_oleskelu- Recreation & leisurehm_oppaat- Guides & publicationshm_ryhmat- Groups & clubshm_tilaisuudet- Events & lectures
Demographics:
- Age groups:
ageGroup/range:18-29,ageGroup/range:30-64, etc.- To specify "appropriate for everyone" I think you have to do 18-29, 30-64, 65-99 separately (because apparently "all ages" wasn't an option in the grand PTV design)
- Gender:
gender/gender(any),gender/male,gender/female
Languages:
fi-FI- Finnishsv- Swedishen- Englishar- Arabicku- Kurdishso-SO- Somaliuk-UA- Ukrainianru-RU- Russianet-EE- Estonianfisl- Finnish Sign Language
Accessibility:
ac_unknow- Organiser does not guarantee accessibilityac_wheelchair- Wheelchair accessibleac_rollator- Rollator accessibleac_inductionloop- Induction loop availableac_itoilet- Accessible toilet availableac_parking- Parking available
Channel type:
place- Physical locationonline- Onlinephone- By phonehybrid- Hybrid (both physical and online)
Geography:
Format: city/COUNTRY/City, state/COUNTRY/State where COUNTRY is FI
city/FI/Helsinkicity/FI/Espoostate/FI/Uusimaastate/FI/Etelä-Savo- etc
Schedule Format
Weekly recurring:
schedule:
start_date: '2026-01-11'
end_date: '2026-05-24'
timezone: Europe/Helsinki
weekly:
- weekday: 2 # Tuesday
start_time: "18:00"
end_time: "19:30"
- weekday: 5 # Friday
start_time: "18:00"
end_time: "19:30"
Weekday numbers are 1-based where 1 is Monday.
There are recurrence periods like P1W for weekly,
and recurrence gaps like P2W... it's pretty confusing ,
but just create it on the server first and then copy the working configuration.
Acknowledgments
Claude Code wrote most of the code. Not without quite a bit of prompting and false starts, mind you.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file lahella_cli-0.1.0.tar.gz.
File metadata
- Download URL: lahella_cli-0.1.0.tar.gz
- Upload date:
- Size: 27.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.18 {"installer":{"name":"uv","version":"0.9.18","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3d5c565592b20672f7b4cf30361b1d40ba42e8e90aa088e5bad33e98e7407b34
|
|
| MD5 |
80556367ba7f7484a290de9147359391
|
|
| BLAKE2b-256 |
6abea6c79341b8b0e5af3c2911ac6e101946a3934d7187c31c5a6559f92e3ca5
|
File details
Details for the file lahella_cli-0.1.0-py3-none-any.whl.
File metadata
- Download URL: lahella_cli-0.1.0-py3-none-any.whl
- Upload date:
- Size: 31.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.18 {"installer":{"name":"uv","version":"0.9.18","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c4526b5e912034d9c1d936fa57ff656dd65290f4595873a640bc450fa32ae015
|
|
| MD5 |
8ea721399e24e6ecff6b1db79e3d50f1
|
|
| BLAKE2b-256 |
4973625940e41c659897a90fe94ebc9fd572e1136464fa1f6e868dd6bc9c3304
|