Filesystem interface for stele1 climbing catalogs
Project description
stele1-filesystem
Read and write stele1 catalogs on disk.
A catalog is a directory whose layout is defined by the
stele1 spec:
one JSON file per record at {type}/{uuid}.json,
photo images at photo/{uuid}_image.{ext},
catalog metadata at stele1.json.
Your data is never held hostage by this library;
the worst-case recovery tool is ls and a text editor.
This package provides two classes with the same verbs:
Catalogspeaks validated stele1 datatypes (Climb,Area,Photo,Trail,Parking,Metadata). It is the one most scripts want.DictCatalogspeaks plain dicts, with no validation beyond well-formed JSON and filename/uuid agreement. It is for recovery, debugging, and bring-your-own-classes tooling.
Use
from uuid import uuid4
from stele1.filesystem import Catalog
from stele1.climb import Climb
from stele1.area import Area
catalog = Catalog('hueco.stele1.d')
if not catalog.exists():
catalog.create()
area = Area(uuid4())
area.name = 'The Big Time'
area.location = {
'type': 'approximation',
'circle': {'latitude': 31.9222, 'longitude': -106.0434,
'meter_radius': 40},
}
catalog.set_area(area)
climb = Climb(uuid4())
climb.name = 'See Spot Run'
climb.rating = {'difficulty': 'V6',
'protection': 'many pads and a good spotter'}
climb.length = {'lower_estimate': 7, 'upper_estimate': 8, 'unit': 'meters'}
climb.location = {'latitude': 31.9222, 'longitude': -106.0434,
'meter_radius': 15}
climb.tags = ['highball', 'classic']
catalog.set_climb(climb)
Nothing persists until a set_* call;
records are just objects.
Reading gives the same objects back:
spot = catalog.get_climb(climb.uuid)
spot.name # 'See Spot Run'
spot.notes = [{'topic': 'crux',
'content': 'the hard moves are low; the top is easier'}]
catalog.set_climb(spot)
The core loop is read, mutate, write back:
for c in catalog.each_climb():
if c.tags and 'highball' in c.tags:
c.fields = {**(c.fields or {}), 'bring': 'many pads'}
catalog.set_climb(c)
Every record type has the same five verbs:
catalog.climb_uuids() # iterate uuids on disk
catalog.get_climb(uuid) # -> Climb; KeyError if absent
catalog.set_climb(climb) # validate and write
catalog.each_climb(on_error=cb) # yield every valid Climb
catalog.purge_climb(uuid) # remove, scrubbing photo references
area mirrors climb, including purge_area.
photo, trail, and parking have plain drop_* instead of purge_*:
nothing references them, so there is nothing to purge.
Errors
Invalid records raise TypeError or ValueError
at the read or write that touched them,
naming the problem:
climb.length = {'lower_estimate': 9, 'upper_estimate': 7, 'unit': 'meters'}
# ValueError: climb .length lower_estimate must not exceed upper_estimate
each_* never dies on one bad file:
unreadable or invalid records are skipped,
and reported through on_error(exception, uuid) if given.
def report(e, uuid):
print(f'skipped {uuid}: {e}')
for c in catalog.each_climb(on_error=report):
...
Removing records
Climbs and areas can be referenced by photo overlay layers, so removing them safely means cleaning those references:
catalog.purge_climb(climb.uuid) # removes the climb AND scrubs
# climb_layers from every photo
catalog.drop_climb_no_purge(uuid) # removes only the record file,
# leaving references dangling;
# the name is long on purpose
Purges are idempotent and silent when the record is already gone. They touch many files and are not atomic; re-run to complete after an interruption.
Photo images
The image itself is a file beside the photo record:
from stele1.photo import Photo
photo = Photo(uuid4())
photo.capture_time = '2026-01'
catalog.set_photo(photo)
catalog.set_photo_image(photo.uuid, 'spot-west-face.jpg') # from a path
catalog.set_photo_image(photo.uuid, image_bytes, 'jpg') # from bytes
catalog.photo_image_path(photo.uuid) # or None
catalog.drop_photo(photo.uuid) # removes record and image
Broken catalogs
Catalog validates,
and some of its checks are deliberately stricter than the spec
(marked beyond spec in the source).
DictCatalog reads and writes the same directory as plain dicts,
so any well-formed catalog always loads:
from stele1.filesystem import DictCatalog
raw = DictCatalog('hueco.stele1.d')
data = raw.get_climb(uuid) # dict, no validation
data['length']['unit'] = 'meters' # repair by hand
raw.set_climb(data) # canonical formatting back out
catalog.get_climb(uuid) # now validates
Constructing a DictCatalog is the deliberate act;
Catalog offers no unvalidated access.
Make it yours
Most projects grow their own conventions: a tag taxonomy, required fields, derived values. The blessed pattern is a thin wrapper, not a subclass:
ALLOWED_TAGS = {'highball', 'classic', 'lowball', 'roof'}
class HuecoCatalog:
def __init__(self, path):
self._c = Catalog(path)
def get_climb(self, uuid):
climb = self._c.get_climb(uuid)
self._check(climb)
return climb
def set_climb(self, climb):
self._check(climb)
self._c.set_climb(climb)
def _check(self, climb):
for tag in climb.tags or ():
if tag not in ALLOWED_TAGS:
raise ValueError(f'{climb.uuid}: unknown tag {tag!r}')
def __getattr__(self, name): # everything else passes through
return getattr(self._c, name)
Wrappers can rely on:
records are self-contained (no back-reference to the catalog);
all typed reads funnel through get_*;
formatting is the catalog's job, never yours.
The documented exceptions: purges and DictCatalog
bypass the typed verbs by design.
Formatting
All writes are canonical: sorted keys, 2-space indent, UTF-8, trailing newline. A catalog diffs cleanly under version control regardless of which tool wrote it. The formatter is exported for tooling that writes catalog files without a catalog object:
from stele1.filesystem import dumps_data
dumps_data({'uuid': '...', 'name': 'See Spot Run'})
Install
pip install stele1-filesystem
Depends only on stele1-datatypes; both are stdlib-only.
Development
Source and issues live at codeberg.org/stele-climbing/stele1.
License
MIT
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 stele1_filesystem-0.3.8.tar.gz.
File metadata
- Download URL: stele1_filesystem-0.3.8.tar.gz
- Upload date:
- Size: 15.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0a1306e50f8db58ee069d2b51e0c96845d2cab2773055eb1c44581ec6936991e
|
|
| MD5 |
b9f5b7bf38eede9724a2124e81e6fb90
|
|
| BLAKE2b-256 |
fac77f90efe18559502ddc85130d205caf925ff0e3b733ae670e3bdb29aab91e
|
File details
Details for the file stele1_filesystem-0.3.8-py3-none-any.whl.
File metadata
- Download URL: stele1_filesystem-0.3.8-py3-none-any.whl
- Upload date:
- Size: 14.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6444f032b0d63bcce0c9ee295e8faa5ee857404cb79e4df33d7aa447e5b08e20
|
|
| MD5 |
09bfa2fba21d23b111b96eca56d385bc
|
|
| BLAKE2b-256 |
93a1da69aa11ea732a709a21cf6ee7a04996d4a6069818a076412690ecc261b2
|