Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion docs/python.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,11 @@ m.on_layer_change(lambda e: print("layers", e["layerIds"]))
| `add_colormap(colormap, vmin=, vmax=, label=, **kwargs)` | Add a colorbar from a named colormap (leafmap-style alias of `add_colorbar`). |
| `set_center(lng, lat, zoom=None)` | Center (and optionally zoom) the map. |
| `set_center_zoom(lng, lat, zoom=None)` | Alias of `set_center` (leafmap compatibility). |
| `remove_layer(layer_id)` / `clear_layers()` | Remove layers. |
| `set_zoom(zoom)` / `set_bearing(bearing)` / `set_pitch(pitch)` / `fit_project_bounds(bounds)` | Persist camera changes without requiring the widget to be displayed. |
| `center` / `zoom` / `bearing` / `pitch` / `basemap` / `name` | Read persisted project and camera state; `name` is writable. |
| `rename_layer(layer, name)` / `move_layer(layer, index)` / `duplicate_layer(layer, name=)` / `show_layer(layer)` / `hide_layer(layer)` | Manage layers by id, name, or `Layer` handle. |
| `layer_properties(layer)` / `column_values(layer, column)` / `describe()` | Inspect inlined data and summarize a project without a browser round trip. |
| `remove_layer(layer_id)` / `clear_layers()` | Remove one layer by id, name, or handle, or remove all layers. |
Comment thread
coderabbitai[bot] marked this conversation as resolved.
| `to_project(keep_credentials=False)` | Return the current project as a dict, credentials redacted unless `keep_credentials=True`. |
| `load_project(src)` | Replace the project from a dict, JSON string, or `.geolibre.json` path. |
| `save_project(path, keep_credentials=False)` | Write the current project to a `.geolibre.json` file, credentials redacted unless `keep_credentials=True`. |
Expand Down
6 changes: 6 additions & 0 deletions python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,12 @@ print(describe_project(project))
save_project("copy.geolibre.json", project)
```

These are the lossless file primitives: unlike `Map.save_project`, the top-level
`save_project` writes the project **verbatim**, credentials included, so that
editing a project in place cannot strip your own API keys out of it. Pass a
project through `geolibre.project.redact_credentials` first if the file is going
anywhere untrusted, or use `Map.save_project`, which redacts by default.

## Notes

- The bundled app is served from a localhost HTTP server, so the interactive
Expand Down
12 changes: 11 additions & 1 deletion python/src/geolibre/authoring.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,9 +115,19 @@ def save_project(path: str | Path, project: dict[str, Any]) -> Path:
can approach ``MAX_PROJECT_BYTES``, and the MCP server rewrites the whole
file on every edit, so a truncating write is a real way to lose work.

Note:
This writes *verbatim*, credentials included. It is the lossless
primitive the MCP server round-trips a user's own project file through,
where stripping an API key on every small edit would quietly destroy the
file's usefulness. :meth:`geolibre.Map.save_project` is the counterpart
for producing a file to share: it redacts unless
``keep_credentials=True``. Run a project through
:func:`geolibre.project.redact_credentials` before calling this if the
result is going anywhere untrusted.

Args:
path: Destination path.
project: The project dict to serialize.
project: The project dict to serialize, written as given.

Returns:
The resolved path written to.
Expand Down
4 changes: 3 additions & 1 deletion python/src/geolibre/geolibre.py
Original file line number Diff line number Diff line change
Expand Up @@ -2525,7 +2525,9 @@ def source(self) -> Any:
into an output that often gets committed or shared. Read
:attr:`Map.project` for the record exactly as stored.
"""
return _project.redact_layer(self._layer()).get("source")
# Sweep the one field rather than the whole layer: `redact_layer` would
# copy an inlined geojson blob first, only to discard it here.
return _project.redact_layer_field(self._layer().get("source"))

@property
def data(self) -> dict[str, Any]:
Expand Down
9 changes: 9 additions & 0 deletions python/src/geolibre/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,15 @@ def _sweep_layer_credentials(layer: dict[str, Any]) -> None:
layer[field] = _redact_config(layer[field])


def redact_layer_field(value: Any) -> Any:
"""Return one of a layer's config fields, detached and swept.

The single-field counterpart to :func:`redact_layer`, for a read that wants
only ``source`` and should not pay to copy an inlined GeoJSON blob first.
"""
return _redact_config(value)


def redact_layer(layer: dict[str, Any]) -> dict[str, Any]:
"""Return a detached copy of one layer, safe to display or hand to others.

Expand Down
Loading