Skip to main content

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:

  • Catalog speaks validated stele1 datatypes (Climb, Area, Photo, Trail, Parking, Metadata). It is the one most scripts want.
  • DictCatalog speaks 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

stele1_filesystem-0.3.8.tar.gz (15.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

stele1_filesystem-0.3.8-py3-none-any.whl (14.2 kB view details)

Uploaded Python 3

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

Hashes for stele1_filesystem-0.3.8.tar.gz
Algorithm Hash digest
SHA256 0a1306e50f8db58ee069d2b51e0c96845d2cab2773055eb1c44581ec6936991e
MD5 b9f5b7bf38eede9724a2124e81e6fb90
BLAKE2b-256 fac77f90efe18559502ddc85130d205caf925ff0e3b733ae670e3bdb29aab91e

See more details on using hashes here.

File details

Details for the file stele1_filesystem-0.3.8-py3-none-any.whl.

File metadata

File hashes

Hashes for stele1_filesystem-0.3.8-py3-none-any.whl
Algorithm Hash digest
SHA256 6444f032b0d63bcce0c9ee295e8faa5ee857404cb79e4df33d7aa447e5b08e20
MD5 09bfa2fba21d23b111b96eca56d385bc
BLAKE2b-256 93a1da69aa11ea732a709a21cf6ee7a04996d4a6069818a076412690ecc261b2

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page