Skip to main content

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

Metadata

Release files for stele1-filesystem 0.3.8

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for stele1-filesystem 0.3.8
File Size Uploaded Provenance
stele1_filesystem-0.3.8.tar.gz 15.0 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for stele1-filesystem 0.3.8
File Interpreter ABI Platform
stele1_filesystem-0.3.8-py3-none-any.whl Python 3 none any Details

Total release size: 29.2 kB

Release files / stele1_filesystem-0.3.8.tar.gz

Download URL stele1_filesystem-0.3.8.tar.gz
Size 15.0 kB
Tags Source
SHA-256 checksum
How to use checksums
0a1306e50f8db58ee069d2b51e0c96845d2cab2773055eb1c44581ec6936991e
BLAKE2b-256 checksum
How to use checksums
fac77f90efe18559502ddc85130d205caf925ff0e3b733ae670e3bdb29aab91e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.14.6

Release files / stele1_filesystem-0.3.8-py3-none-any.whl

Download URL stele1_filesystem-0.3.8-py3-none-any.whl
Size 14.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
6444f032b0d63bcce0c9ee295e8faa5ee857404cb79e4df33d7aa447e5b08e20
BLAKE2b-256 checksum
How to use checksums
93a1da69aa11ea732a709a21cf6ee7a04996d4a6069818a076412690ecc261b2
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.14.6

Security for stele1-filesystem 0.3.8

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page