Skip to content

feat(python): expand layer management and headless camera API - #1770

Merged
giswqs merged 8 commits into
mainfrom
feat/python-layer-and-camera-api
Aug 8, 2026
Merged

feat(python): expand layer management and headless camera API#1770
giswqs merged 8 commits into
mainfrom
feat/python-layer-and-camera-api

Conversation

@giswqs

@giswqs giswqs commented Aug 8, 2026

Copy link
Copy Markdown
Member

Summary

  • Add layer management to Map (rename_layer, move_layer, duplicate_layer, show_layer, hide_layer) and let remove_layer accept an id, display name, or Layer handle like the other layer methods.
  • Add introspection without a browser round trip: layer_properties, column_values, describe, plus Layer.source / .data / .index / .properties() / .column() / .move() / .duplicate().
  • Add persisted camera and metadata access (set_zoom, set_bearing, set_pitch, fit_project_bounds, and center / zoom / bearing / pitch / basemap / name properties), and export the headless authoring helpers (load_project, save_project, describe_project, basemap_catalog, builtin_legend_names, color_ramp_names) from the top-level geolibre package.

Behavior changes worth a reviewer's attention

  • Map._resolve_layer now delegates to authoring.find_layer, so every method that takes a layer reference (including the pre-existing set_layer_visibility, set_layer_opacity, zoom_to_layer) resolves an id first, then an exact name, then a case-insensitive one. A name that several layers share now raises instead of silently picking the first match. This makes the scripting API and the MCP tools agree on what a reference means. Map.find_layer keeps its first-match / None contract for leafmap compatibility.
  • remove_layer on an unknown reference now raises ValueError where it used to be a silent no-op. A caller that wants the old semantics can guard with if m.find_layer(name):.
  • Map.basemap, Layer.source, and Layer.data redact credentials, matching what to_project() and save_project() already did. A notebook auto-displays whatever a cell returns, so an API key in a style URL or a request_headers blob would otherwise print into an output that often gets committed. Map.project still returns everything exactly as stored.
  • set_center now routes through authoring.set_view, so recentering clears the bbox a previous fit_project_bounds recorded rather than leaving it describing a stale extent.

Test plan

  • cd python && pytest (257 passed, 3 skipped)
  • pre-commit run --files <changed files> clean, including the npm build hook
  • CI: Python package tests pass on 3.11 through 3.14
  • Spot check in a notebook: build a map, duplicate and reorder a layer, read a column, then save_project and reopen it

Scripting a map from Python needed a browser round trip or a raw project
dict for common tasks: reordering, duplicating, renaming layers, reading
attribute values, and moving the camera. These now work as plain project
mutations on Map and Layer, and the project authoring helpers are exported
from the top-level package for scripts that never display a widget.
Copilot AI lite review requested due to automatic review settings August 8, 2026 04:06

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The Python API adds layer management and inspection helpers, persisted camera and project metadata APIs, top-level authoring exports, documentation, and tests for headless project operations.

Changes

Python authoring API

Layer / File(s) Summary
Layer management and handles
python/src/geolibre/geolibre.py, python/tests/test_scripting.py, python/README.md
Map and Layer support shared reference resolution, renaming, ordering, visibility, duplication, removal, property inspection, column extraction, and metadata access.
Camera and project state
python/src/geolibre/geolibre.py, python/tests/test_scripting.py, python/README.md
Map exposes persisted camera settings, project bounds fitting, basemap metadata, and validated project names.
Headless utilities and public exports
python/src/geolibre/__init__.py, python/tests/test_project.py, python/README.md
Top-level catalog, project description, loading, and saving utilities are exported, documented, and tested.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant Map
  participant AuthoringResolver
  participant Layer
  Caller->>Map: Resolve layer reference
  Map->>AuthoringResolver: Match ID or name
  AuthoringResolver-->>Map: Return layer record
  Map->>Layer: Apply layer operation
  Layer-->>Caller: Return updated state
Loading

Possibly related PRs

Poem

A rabbit checks each layer’s name,
Then moves and copies maps the same.
Cameras save their settled view,
Projects load and exports do too.
Tests hop cleanly through the queue.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 76.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main Python API changes for layer management and headless camera control.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/python-layer-and-camera-api

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@python/README.md`:
- Around line 99-106: Create the “Roads” layer before calling
find_layer("Roads") in the README example, or explicitly document that an
existing layer is required, ensuring the snippet does not dereference None when
no such layer exists.

In `@python/src/geolibre/__init__.py`:
- Around line 16-27: Reorder the exports in __all__ so "__version__" appears
before the lowercase names, satisfying Ruff RUF022 while preserving all existing
exports.

In `@python/src/geolibre/geolibre.py`:
- Around line 2469-2471: Update Layer.index to validate the handle by calling
self._layer() before searching the map layers, ensuring removed layers raise the
same documented ValueError as other Layer accessors instead of StopIteration.
- Around line 1000-1005: Update duplicate_layer to validate the requested name
through the same reserved-name validation path used by rename_layer, before
calling _add_layer; ensure name="__basemap__" is rejected while existing
default-name behavior remains unchanged, and add a regression test covering this
reserved name.
- Around line 979-1025: Update Map._resolve_layer and the removal path around
python/src/geolibre/geolibre.py:1959-1960 to resolve string references through
_authoring.find_layer before constructing or using a Layer handle, preserving
case-insensitive matching and duplicate-name rejection for all mutations and
removals. Add coverage in python/tests/test_scripting.py:627-655 for duplicate
display names and case-insensitive references.
- Around line 2017-2023: Update set_center in python/src/geolibre/geolibre.py
around lines 2017-2023 to route map updates through _authoring.set_view instead
of writing mapView directly, ensuring a manual recenter clears any persisted
bbox. Add or update the test in python/tests/test_scripting.py around lines
692-695 to fit project bounds, call set_center, and assert mapView no longer
contains bbox.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: d3080458-dde6-4daf-ae57-c3381c6502cf

📥 Commits

Reviewing files that changed from the base of the PR and between d8b06e9 and d1a8e3c.

📒 Files selected for processing (5)
  • python/README.md
  • python/src/geolibre/__init__.py
  • python/src/geolibre/geolibre.py
  • python/tests/test_project.py
  • python/tests/test_scripting.py

Comment thread python/README.md Outdated
Comment thread python/src/geolibre/__init__.py
Comment thread python/src/geolibre/geolibre.py Outdated
Comment thread python/src/geolibre/geolibre.py Outdated
Comment thread python/src/geolibre/geolibre.py
Comment thread python/src/geolibre/geolibre.py
@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

🔍 Cloudflare PR preview

Item Value
Site https://faef3ca3.geolibre-preview.pages.dev
Demo app https://faef3ca3.geolibre-preview.pages.dev/demo/
Commit ca95bd2

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

🔍 GitHub Pages PR preview

Item Value
Site https://opengeos.org/pages-preview/GeoLibre/pr-1770/
Demo app https://opengeos.org/pages-preview/GeoLibre/pr-1770/demo/
Commit ca95bd2

Note

GitHub Pages built this preview successfully, but its serving edge returned HTTP 403 when checked. The links may still be propagating.

Comment thread python/src/geolibre/geolibre.py
Comment thread python/src/geolibre/geolibre.py Outdated
Comment thread python/src/geolibre/geolibre.py
@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Code review

Bugs

  • Layer.index (python/src/geolibre/geolibre.py:2468-2471) uses next(...) with no default, so a stale handle (layer removed) raises a bare StopIteration instead of the ValueError every sibling property (.name, .style, .source, .data, etc.) raises via self._layer(). Confidence: medium-high.

Security

  • None found.

Performance

  • Map.describe() deep-copies the entire project (including any large inlined GeoJSON on every layer) before summarizing, even though describe_project only needs isolation for the small mapView sub-dict (its layers entries are already freshly built summary dicts). The deepcopy is a defensible correctness choice — without it, describe()["mapView"] would leak a live reference into the map's internal state — but it's more expensive than necessary for large projects. Confidence: low.

Quality

  • move_layer's docstring claims negative indices "follow normal Python insertion semantics," but real list.insert(-1, x) inserts before the last element rather than at the end; the actual behavior (translate -1len - 1 pre-removal) is closer to indexing semantics and does land at the true end, but the comparison could mislead a reader. Confidence: low.
  • set_zoom/set_bearing/set_pitch were all added, but only zoom got a matching read property (alongside center/basemap/name) — no bearing/pitch getters, so callers must reach into m.project["mapView"]["bearing"] directly, as the new test does. Minor asymmetry in the new "headless camera API." Confidence: low-medium.
  • remove_layer's parameter is still named layer_id even though it now accepts an id, display name, or Layer handle, unlike the new sibling methods (rename_layer(layer, ...), move_layer(layer, ...)) which use layer. Confidence: low.
  • Validation is inconsistent across the new naming APIs: Map.name setter rejects blank/empty strings, but rename_layer/Layer.name (routed through _authoring.update_layer) and duplicate_layer(name="") silently accept or fall back on an empty name rather than raising. Confidence: low.

CLAUDE.md

  • No violations found — the change is scoped to python/, doesn't touch any of the mirrored-constant files called out in CLAUDE.md, and the README/test updates follow existing conventions.

I checked the resolve/mutate plumbing (_resolve_layer, _update_project, _add_layer), the pre-existing authoring.py functions being newly exposed (update_layer, remove_layer, layer_properties, column_values, describe_project, set_view, fit_bounds) for correctness of the new call sites, the top-level __init__.py exports for import-cycle issues, and the new/changed tests for coverage — no other correctness, security, or CLAUDE.md issues stood out.

- Route Map._resolve_layer through authoring.find_layer so scripting and the
  MCP tools agree on what a layer reference means: id first, then exact name,
  then case-insensitive, with a duplicated name raising instead of silently
  picking one. Now that remove_layer accepts names, an arbitrary pick would
  delete the wrong layer.
- Validate duplicate_layer's explicit name against the reserved basemap
  pseudo-id, which rename_layer already rejects via update_layer.
- Route set_center through authoring.set_view so a manual recenter clears the
  bbox fit_project_bounds recorded, instead of leaving a stale extent behind.
- Make Layer.index raise the documented ValueError for a removed handle rather
  than a bare StopIteration, matching the other accessors.
- Add Map.bearing and Map.pitch read properties so every camera setter has a
  matching getter.
- Correct the move_layer docstring: negative values follow sequence indexing,
  not list.insert semantics.
- Sort __version__ into __all__ (RUF022) and make the README layer-handle
  example self-contained.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
python/README.md (1)

92-92: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Document the bearing and pitch properties.

The persisted-state property list omits bearing and pitch. Include both getters so users can discover how to read persisted camera orientation.

Proposed fix
-| `center` / `zoom` / `basemap` / `name` | Read persisted project and camera state; `name` is writable. |
+| `center` / `zoom` / `bearing` / `pitch` / `basemap` / `name` | Read persisted project and camera state; `name` is writable. |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/README.md` at line 92, Update the persisted-state property list in the
README to include the read-only bearing and pitch getters alongside center,
zoom, basemap, and name, while preserving the existing note that only name is
writable.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@python/tests/test_scripting.py`:
- Around line 676-700: Resolve the Ruff violations in the tests: inside the
pytest.raises block around duplicate.index, assign the property result to _
while preserving the expected ValueError. In
test_duplicate_layer_rejects_the_reserved_basemap_name, constrain pytest.raises
with a match for the reserved-name error message instead of accepting any
ValueError.

---

Outside diff comments:
In `@python/README.md`:
- Line 92: Update the persisted-state property list in the README to include the
read-only bearing and pitch getters alongside center, zoom, basemap, and name,
while preserving the existing note that only name is writable.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 201e895d-9a8b-4108-a7ac-0d7d1fb7051e

📥 Commits

Reviewing files that changed from the base of the PR and between d1a8e3c and f48f582.

📒 Files selected for processing (4)
  • python/README.md
  • python/src/geolibre/__init__.py
  • python/src/geolibre/geolibre.py
  • python/tests/test_scripting.py

Comment thread python/tests/test_scripting.py
- Bind the bare `duplicate.index` access to `_` so the statement reads as a
  deliberate property evaluation rather than dead code.
- Match on the reserved-name message instead of accepting any ValueError, so
  the test cannot pass on an unrelated failure.
Comment thread python/src/geolibre/geolibre.py Outdated
Comment thread python/src/geolibre/geolibre.py Outdated
Comment thread python/src/geolibre/geolibre.py Outdated
@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Code review

Bugs

  • duplicate_layer's name fallback (name or f"...") treats an explicitly-passed name="" the same as None, silently discarding it in favor of the auto-generated "<original> copy" name — inconsistent with the is not None guard used for the reserved-name check just above it, and with Map.name's own setter in this PR, which explicitly rejects blank strings instead of silently defaulting. Confidence: medium-low (python/src/geolibre/geolibre.py:1012).

Security

  • None found. No injection, unsafe deserialization, or credential exposure in the diff.

Performance

  • Map.describe() deep-copies the entire project (including any inlined GeoJSON, up to the 256 MB project cap) before delegating to _authoring.describe_project, which only produces a small id/name/type/visibility/featureCount summary. Sibling methods layer_properties/column_values added in the same PR pass the live dict through without copying, and the only mutation the deep copy guards against (layers_of() defaulting a missing layers list) never occurs for a project built through this API. This undercuts the PR's stated goal of avoiding costly round trips for large projects. Confidence: medium (python/src/geolibre/geolibre.py:1033).

Quality

  • duplicate_layer reaches into _authoring._reject_reserved_name, a private (underscore-prefixed) helper of another module; the accompanying comment explains why, but it's a leaky abstraction that must be kept in sync by hand. Confidence: low.
  • remove_layer's docstring lacks a Raises: section even though it now raises ValueError for an unresolvable reference (a deliberate, tested behavior change from the old silent no-op), unlike neighboring methods that document their Raises. Confidence: low.
  • No test exercises move_layer with an actual negative index, despite the docstring calling out specific -1 semantics distinct from list.insert(-1, ...); worth a regression test given how easy that off-by-one class of bug is to reintroduce. Confidence: low.

CLAUDE.md

  • No violations found. This PR only touches python/ (the headless geolibre package and its tests/README); none of the JS/TS/Tauri/CLAUDE.md-specific conventions (mirrored constants, i18n, RTL, Whitebox catalog, etc.) apply here.

I also verified that the refactored _resolve_layer (now delegating to _authoring.find_layer), move_layer's negative-index math, fit_project_bounds, and the new camera properties are all logically consistent with their underlying authoring.py implementations and are backed by passing tests in test_scripting.py/test_project.py.

- Route duplicate_layer through authoring.add_layer instead of reaching into
  the private _reject_reserved_name, and reject a blank explicit name rather
  than silently substituting the auto-generated one, matching the Map.name
  setter.
- Stop deep-copying the whole project in describe(): copy the small summary
  instead, which detaches the live mapView it returns without duplicating
  every inlined GeoJSON blob to report a feature count.
- Document that remove_layer now raises on an unresolved or ambiguous
  reference, where it used to be a silent no-op.
- Cover move_layer's negative-index semantics, which differ from
  list.insert(-1, ...) and are easy to reintroduce a bug in.
@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

This all checks out. I've done a thorough trace of the new methods, verified the negative-index math in move_layer against the tests, confirmed _update_project's deep-copy-then-mutate pattern makes every new method exception-safe (no partial writes on ValueError), and cross-checked the new Map/Layer surface against the pre-existing authoring.py module it delegates to. I did not find any bugs, security issues, or CLAUDE.md violations — only a couple of very minor style nits worth a light mention.

Code review

Bugs: None found. Traced move_layer's negative-index handling against all three test cases (including double negative offsets and out-of-range clamping) and confirmed the math is correct; confirmed _update_project's deep-copy-then-mutate pattern (geolibre.py:409-422) means a ValueError raised mid-mutation (e.g. the reserved-name check in duplicate_layer) never leaves the project partially modified.

Security: None found. All new methods operate on in-memory dicts; no new file I/O, network calls, or string interpolation into commands/queries.

Performance: None found. Layer.data/duplicate_layer deep-copy the full layer record (including inlined GeoJSON), but that's inherent to the feature (returning/duplicating the layer's data) rather than an inefficiency introduced carelessly.

Quality:

  • Low confidence: duplicate_layer's explicit name isn't .strip()-ed before being stored (geolibre.py:148-156), unlike the Map.name setter which does strip. A name like " Clone " would pass the non-empty check but be stored with surrounding whitespace intact. Minor inconsistency, unlikely to matter in practice.
  • Low confidence: remove_layer changes from a silent no-op on an unknown id to raising ValueError (geolibre.py:189-209). This is clearly intentional and documented in the docstring, but it's a breaking behavior change for any existing caller relying on idempotent "remove if present" semantics.
  • Low confidence: duplicate_layer always appends the copy at the end of the draw order rather than next to the source layer, which may surprise users expecting the duplicate to sit adjacent to the original. Not documented either way, but plausible as intentional ("duplicate always drawn on top").

CLAUDE.md: No violations found. The new Map/Layer methods correctly delegate to the existing authoring.py module rather than duplicating logic, consistent with the documented "Map and MCP tools both delegate to authoring.py" architecture. Docstrings, tests, and README updates are all present as expected for a Python API change.

- Strip an explicit duplicate_layer name before storing it, matching the
  Map.name setter, so "  Clone  " does not become a padded name that is
  awkward to reference back.
- Document that the copy is appended to the draw order rather than placed
  next to its source, which was previously unstated either way.
@giswqs

giswqs commented Aug 8, 2026

Copy link
Copy Markdown
Member Author

Addressing the three quality nits from the latest review summary (they were not attached to lines, so replying here):

  • duplicate_layer does not strip the name. Fixed in f0bfb93. The explicit name is now stripped before the blank check, so " Clone " stores as "Clone" and " " still raises, matching the Map.name setter.
  • duplicate_layer appends rather than sitting next to the source. Documented in f0bfb93 rather than changed. Appending matches authoring.add_layer's default and the rest of the add path, so the copy lands on top like any newly added layer; move_layer puts it elsewhere.
  • remove_layer now raises instead of silently no-opping. Keeping the new behavior. A remove that quietly does nothing hides a typo, and now that the method accepts display names the silent path would also swallow an ambiguous name. It is documented under Raises:, and callers wanting the old semantics can guard with find_layer(...) is not None.

Comment thread python/src/geolibre/geolibre.py Outdated
Comment thread python/src/geolibre/geolibre.py Outdated
Comment thread python/README.md Outdated
Comment thread python/src/geolibre/geolibre.py
@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Code review

Bugs

  • None found. The new layer-management/camera methods (move_layer's negative-index handling, duplicate_layer's id/name assignment, _resolve_layer's delegation to the shared authoring.find_layer) checked out correctly against their tests and the underlying authoring.py helpers.

Security

  • Layer.source / Layer.data return the raw layer dict unredacted, including source["requestHeaders"] for WMS/WFS/tile layers with auth headers — unlike to_project()/save_project(), which explicitly run redact_credentials before a project leaves the process. Since these are new, easily-discoverable properties whose return value auto-displays in Jupyter, they meaningfully raise the odds of accidental credential disclosure via notebook output. Confidence: medium.

Performance

  • None found. duplicate_layer deep-copies the full source layer (including any inlined GeoJSON), which is inherent to the feature and not flagged as an issue.

Quality

  • rename_layer (and Layer.name's setter, which now routes through it) doesn't reject a blank/whitespace-only name, while two sibling APIs added in the same PR — Map.name's setter and duplicate_layer — do. Inconsistent validation across otherwise-parallel methods. Confidence: medium.
  • Map.remove_layer's parameter is still named layer_id even though it now accepts an id, name, or Layer handle, unlike every other sibling method in this PR which uses layer. Cosmetic only. Confidence: low.
  • The behavior change where remove_layer now raises on an unmatched/ambiguous reference instead of silently no-op'ing is well-documented in the docstring and tests, but isn't called out in the PR description — worth a mention for anyone relying on the old silent-no-op semantics. Confidence: low.

CLAUDE.md

  • No violations found; changes are confined to python/, which isn't covered by the JS/desktop-specific conventions in CLAUDE.md.

Posted 4 inline comments (1 medium-confidence security note, 1 medium-confidence validation-inconsistency bug, 2 low-confidence documentation/naming nits).

- Sweep credentials from Layer.source and Layer.data. Both hand a layer record
  straight to a caller, and a notebook auto-displays whatever a cell returns,
  so a source built with request_headers or a signed URL would print its
  secrets into an output that often gets committed. Factored the per-layer
  sweep redact_credentials already ran into project.redact_layer so the two
  paths cannot drift.
- Reject a blank name in rename_layer (and so in the Layer.name setter, which
  delegates to it); authoring.update_layer guards only the reserved basemap
  pseudo-id. Shares one _clean_layer_name helper with duplicate_layer.
- List the bearing and pitch read properties in the README table.
Comment thread python/src/geolibre/geolibre.py Outdated
Comment thread python/src/geolibre/geolibre.py
Comment thread python/src/geolibre/geolibre.py
@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Code review

Bugs

  • None found. Traced move_layer's negative-index clamping, duplicate_layer's deep-copy/id-swap, rename_layer/duplicate_layer's reserved-name and blank-name validation, and the transactional behavior of _update_project (a raise inside the mutate callback leaves self.project untouched) — all check out against the new tests and each other.

Security

  • Map.basemap (geolibre.py:2092-2095) returns the raw basemapStyleUrl without redaction, unlike Layer.source/Layer.data added in this same PR (which explicitly redact credentials because "a notebook auto-displays whatever a cell returns") and unlike to_project()/save_project(), which already redact basemapStyleUrl via _redact_url. Since basemap style URLs from some providers embed an API key as a query parameter, this looks like an inconsistent gap in the PR's own stated redaction rationale. Confidence: medium-high. (inline comment posted)

Performance

  • Layer.data (geolibre.py:2526-2531) deep-copies the full layer record including any inlined GeoJSON, unlike authoring.layer_summary/describe_project, which intentionally avoid echoing potentially tens-of-megabytes of embedded features. Could be a surprising cost/notebook-output-size issue if this is not the intended tradeoff. Confidence: low-medium. (inline comment posted)

Quality

  • _resolve_layer's switch to authoring.find_layer (geolibre.py:963-968) changes behavior for pre-existing methods too (set_layer_visibility, set_layer_opacity, zoom_to_layer, identify), not just the new ones: an ambiguous name now raises instead of silently picking the first match, and (via the rewritten remove_layer) removing an unknown reference now raises instead of no-op'ing. This appears deliberate and is well-documented/tested, but is a behavior change beyond the new API surface — flagging for confirmation it's acceptable for existing callers. Confidence: medium. (inline comment posted)
  • docs/python.md's Map API reference table (line 256) still shows only remove_layer(layer_id) / clear_layers() and hasn't been updated with the new rename_layer/move_layer/duplicate_layer/show_layer/hide_layer/layer_properties/column_values/describe/camera properties/set_zoom/set_bearing/set_pitch/fit_project_bounds that were added to python/README.md. Not an inline comment since the file isn't part of this diff, but worth a follow-up so the two references don't drift. Confidence: high (verified by reading the file).

CLAUDE.md

  • No applicable violations found; this PR touches only the python/ package, which isn't covered by the JS/desktop-specific conventions (i18n, Whitebox catalog, CSP allowlist, etc.) in the root CLAUDE.md.

- Redact Map.basemap the way Layer.source is redacted. MapTiler and Stadia put
  an API key in the style URL itself, so reading it back in a notebook printed
  the key; project.redact_url is the public entry point for the sweep
  redact_credentials already applied to that field.
- Document that Layer.data copies an inlined geojson blob whole, and point at
  properties()/describe() for the cases a summary covers.
Comment thread python/src/geolibre/geolibre.py
@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Code review

Security

  • Map.describe() (python/src/geolibre/geolibre.py:1063-1069) returns the project summary from _authoring.describe_project() unredacted — layer_summary()'s source field and describe_project()'s basemapStyleUrl field carry raw URLs, including any embedded API keys/tokens. This is inconsistent with Layer.source, Layer.data, and Map.basemap, added in this same PR specifically to strip credentials before handing data back to a notebook cell (which auto-displays return values). describe() is documented as the lightweight, safe alternative to Layer.data for this exact scenario, but it's the one accessor that skips the sweep. Confidence: medium-high. Posted inline with a suggested fix.

Bugs

  • None found with meaningful confidence. Traced move_layer's negative-index math, duplicate_layer's atomicity on validation failure, _resolve_layer's new id→exact-name→case-insensitive-name resolution (shared with authoring.find_layer), and the camera setters (set_zoom/set_bearing/set_pitch/fit_project_bounds) against their tests — all behave as documented and the new tests exercise the edge cases (negative/out-of-range indices, blank/reserved names, ambiguous name matches, redaction of Layer.source/Layer.data).

Performance

  • None found. Layer.data/layer_properties deep-copying whole layers (including inlined GeoJSON) is called out and accepted in the docstrings, with properties()/describe() offered as the lighter alternative.

Quality

  • Map.remove_layer changes from a silent no-op on an unknown id to raising ValueError, and now also accepts a display name (ambiguous names error rather than picking one). This is clearly intentional and documented in the docstring, but it's a behavior change existing callers could depend on for idempotent removal. Confidence: low — flagging for awareness, not as a defect.
  • Minor: remove_layer's parameter is still named layer_id even though it now accepts an id, name, or Layer handle, while its new siblings (rename_layer, move_layer, duplicate_layer) all use layer. Likely deliberate to preserve keyword-arg back-compat on an existing public method. Confidence: low.

CLAUDE.md

  • No violations found. Docstrings/comments follow the repo's style, the Python test suite was extended alongside the change, and README.md was updated to document the new API surface.

Redact URLs in layer_summary and describe_project rather than at the Map.describe
call site. A summary exists to be shown, and both callers show it somewhere
untrusted: a notebook cell that often gets committed, and an MCP tool result that
goes to a model client. Fixing it in authoring.py closes the same gap in the MCP
server's describe_project tool and keeps the one-place-per-change layering the
repo documents.
Comment thread python/README.md
Comment thread python/src/geolibre/geolibre.py
Comment thread python/src/geolibre/geolibre.py
@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Code review

Bugs

  • None found. Traced the new _resolve_layer/authoring.find_layer sharing, the move_layer negative-index/clamp arithmetic (verified by hand against all three test_move_layer_negative_index_counts_from_the_end steps), duplicate_layer's reserved-name/blank-name rejection ordering (fails before any project mutation, via _update_project's deep-copy-then-mutate pattern), and the credential-redaction refactor in project.py (_sweep_layer_credentials extracted cleanly, used identically by redact_credentials and the new redact_layer). All checked out correct.

Security

  • Medium confidence: the newly-exported top-level geolibre.save_project/load_project (authoring.py) do no credential redaction, unlike the pre-existing Map.save_project(path, keep_credentials=False), which redacts by default and documents that as a safety guarantee. The README's own new example loads a project and writes it straight back out with save_project(...), which would silently persist any embedded API keys/requestHeaders — inconsistent with this PR's own redaction work elsewhere (Layer.source, Map.basemap, describe_project). See inline comment on python/README.md.

Performance

  • Medium confidence: Layer.source deep-copies the entire layer record (including up to ~50 MB of inlined GeoJSON) via _project.redact_layer(self._layer()) just to return the small source sub-object. Layer.data intentionally documents this cost; Layer.source doesn't need to pay it. See inline comment on python/src/geolibre/geolibre.py.

Quality

  • Low confidence, nit: remove_layer's parameter is still named layer_id even though it now accepts an id, name, or Layer handle, inconsistent with the layer naming used by every other new method.
  • Low confidence: docs/python.md's Map API table wasn't updated alongside python/README.md's, so it's now stale relative to the new methods/properties added here (rename_layer, move_layer, duplicate_layer, layer_properties, column_values, describe, camera properties, etc.). Noted in the same inline comment since the file isn't part of the diff.

CLAUDE.md

  • No violations found; this PR only touches python/ and doesn't intersect any of the documented mirror/sync obligations (lockfiles, WASM catalogs, i18n, etc.).

@giswqs
giswqs merged commit 81da469 into main Aug 8, 2026
20 checks passed
@giswqs
giswqs deleted the feat/python-layer-and-camera-api branch August 8, 2026 05:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants