Skip to content

Conversation

@juzen2003
Copy link
Collaborator

@juzen2003 juzen2003 commented Sep 29, 2025

This pull is to address #9 and #12, and it's currently in progress.

Changes:

  • 09/20/25:
    • Create a document with steps to run viewmaster locally.
    • Remove (variable HTTPD_CUSTOMIZATION) the step of getting holdings directory from httpd_customization.conf under httpd directory (Apache). Now we will the get the holdings path from an environment variable.
    • Remove the step of adding symlinks to holdings under Webserver/Documents
  • 11/19/25:
    • Update README.md
    • Add docstrings in "Google" format for all files under viewmaster
    • Add Sphinx to generate html documents for all files under viewmaster
      • Update requirments.txt to include Sphinx related packages
      • All Sphinx related files are under rms-viewmaster/docs
      • Generated html documents are under rms-viewmaster/docs/_build
    • Add .readthedocs.yaml for Read the docs (build successed, verified on forked repo)
    • Remove rms-viewmaster/viewmaster/doc
  • 01/10/26:
    • Fixed all the code rabbit comments
    • Removed viewmaster_without_pause.py and its related documentation files.
    • Update the header length of each .rst file to the same as the title length. Note: Sphinx only complains when the header length is shorter than the title length.
    • Update README.md to match the format of the one from rms-starcat
    • Update requirements.txt to have all packages in alphabetical order
    • Update the code in link.py and viewmaster.py to move the step of creating logger from global level to a function. Docstrings are updated correspondingly.

Pending items:

  • create a pyproject.toml file so that we can do pip install rms-viewmaster
  • create a .github directory with the appropriate actions for uploading to PyPI and TestPyPI, running lint, and hopefully some day running tests.

Summary by CodeRabbit

  • New Features

    • Centralized per-request logging and logger-aware cache initialization across endpoints.
    • New grouping and iterator utilities for richer traversal and presentation of directories/files.
  • Documentation

    • Full Sphinx docs added, README expanded, and ReadTheDocs configuration included.
  • Refactor

    • Public APIs updated to accept and propagate a logger; rendering and navigation flows adapted.
  • Chores

    • Simplified configuration and removed legacy testing flags.

✏️ Tip: You can customize this high-level summary in your review settings.

…t setup; add documentation for running Viewmaster locally.
… variable instead of reading HOLDINGS_PATHS in customized httpd conf to obtain the holdings path. Also remove the step of creating symlinks of the holdings under /Library/Webserver/Documents.
…_paths function in viewmaster.py and viewmaster-without-pause.py, streamlining the codebase further.
…repository and correcting the pip install command.
…d update comments in viewmaster.py to reflect the new method of obtaining HOLDINGS_PATHS from the PDS3_HOLDINGS environment variable.
@coderabbitai
Copy link

coderabbitai bot commented Sep 29, 2025

Walkthrough

Centralized, lazily-cached logging was added and threaded through public APIs; holdings-path discovery was simplified to an environment-driven single path; page-cache and initialization were made logger-aware; new grouping and iterator classes were introduced; Sphinx docs, README, and build tooling were added; viewmaster_config testing heuristics simplified.

Changes

Cohort / File(s) Summary
Core logger & public API
viewmaster/viewmaster.py, viewmaster/link.py
Add create_logger() / get_or_create_logger(); replace module-global LOGGER usage with logger argument threading; public functions updated to accept logger; add get_page_cache(logger) and wire into initialization.
Holdings path & validation
viewmaster/viewmaster.py
Replace get_holdings_paths() with get_holdings_path(logger) returning a single env-derived path; update validate_holdings_paths(..., logger) to log via provided logger and raise on critical errors.
Caching & initialization
viewmaster/viewmaster.py
initialize_caches(reset=...) and fill_page_cache(logger) updated to propagate logger; conditional page-cache clearing wired to get_page_cache(logger).
Navigation, rendering & APIs
viewmaster/viewmaster.py, viewmaster/...
Thread logger through navigation (prev/next), directory/product page generation, HTML rendering, and related public APIs (signatures changed to accept logger); replace direct LOGGER references.
Routing & link refinements
viewmaster/link.py
Route handler refactored to use per-request logger, improved redirect and error logging, relative imports for config, and lazy/cached logger initialization in module.
Configuration changes
viewmaster/viewmaster_config.py
Simplify VIEWMASTER_TESTING heuristic to sys.argv[0].endswith('viewmaster.py'); remove VIEWMASTER_FOR_MARK and HTTPD_CUSTOMIZATION; adjust memcached port/socket defaults and testing-mode overrides.
Grouping data structures
viewmaster/pdsgroup.py, viewmaster/pdsgrouptable.py
Add PdsGroup and expand PdsGroupTable with safer None defaults, richer docstrings, merge/insert/remove/hide APIs, group_children(), remove_hidden(), merge_index_row_tables(), and enhanced sorting/visibility helpers.
Iterators
viewmaster/pdsiterator.py
Add PdsDirIterator, PdsFileIterator, PdsRowIterator, plus dirs_only(); implement traversal, copy, neighbor/cousin navigation semantics, filtering, and detailed docstrings.
Documentation & tooling
.readthedocs.yaml, docs/conf.py, docs/Makefile, docs/make.bat, docs/*.rst
Add Read the Docs config, Sphinx conf, Makefile and Windows build script, and multiple RST modules with automodule entries for new/changed modules.
Project docs & deps
README.md, requirements.txt
Replace README with expanded usage/contributing content; add Sphinx-related dependencies (myst-parser, sphinx, sphinxcontrib-napoleon, sphinx-rtd-theme).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive The title "Clean up viewmaster (WIP)" is vague and generic, using non-descriptive terms that don't clearly convey the main changes in this substantial refactoring. Replace with a more specific title that highlights the primary change, such as "Refactor logging infrastructure and holdings path management" or "Centralize logger initialization and configuration."
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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 and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

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

⚠️ Outside diff range comments (18)
viewmaster/viewmaster-without-pause.py (6)

886-889: cgi.escape is removed in modern Python; switch to html.escape.

This will raise AttributeError on Python 3.8+. Replace cgi.escape with html.escape and import html.

-import cgi
+import html
@@
-    if params['selection']:
-        anchor_suffix = '#' + cgi.escape(params['selection'], quote=True)
+    if params['selection']:
+        anchor_suffix = '#' + html.escape(params['selection'], quote=True)
@@
-    if params['selection']:
-        selection_escaped = cgi.escape(params['selection'], quote=True)
+    if params['selection']:
+        selection_escaped = html.escape(params['selection'], quote=True)

Also applies to: 1678-1693


1712-1714: Fix ValidationError import and reference.

wtforms.validators.ValidationError is used but wtforms isn’t imported; this will NameError. Import the symbol directly and reference it.

-from wtforms import StringField, HiddenField
+from wtforms import StringField, HiddenField
+from wtforms.validators import ValidationError
@@
-    if matchobj is None:
-        raise wtforms.validators.ValidationError("")
+    if matchobj is None:
+        raise ValidationError("")

Also applies to: 3-5


1493-1505: Bug: misspelled key and wrong return value.

defaults[keuy] and return clean_query_params(params) are incorrect.

-    new_params = params.copy()
+    new_params = params.copy()
@@
-    for key in defaults:
-        if key not in new_params:
-            new_params[key] = defaults[keuy]
+    for key in defaults:
+        if key not in new_params:
+            new_params[key] = defaults[key]
 
-    return clean_query_params(params)
+    return clean_query_params(new_params)

522-526: Undefined variable row_pdsfile.

Should copy the current query pdsfile, not an undefined name. This currently crashes on the nonexistent index-row path.

-        row_pdsfile = row_pdsfile.copy()
+        row_pdsfile = query_pdsfile.copy()

297-305: Consider gating symlink logic and unused function.

create_holdings_symlinks remains but is no longer used. Either remove or clearly mark as legacy to avoid confusion.


203-205: Prefer LOGGER.warning over warn.

If pdslogger mirrors stdlib, use warning() to align with modern APIs.

Also applies to: 297-305, 297-305

viewmaster/viewmaster_config.py (2)

32-39: Testing config overridden on Linux (memcached sockets).

On Linux, the block unconditionally sets memcache sockets, overriding the earlier testing “0” settings. This enables memcache during tests unintentionally.

-if platform.system() == 'Linux':
-    VIEWMASTER_MEMCACHE_PORT = '/var/run/memcached/memcached.socket'
-    PDSFILE_MEMCACHE_PORT = '/var/run/memcached/memcached.socket'
+if platform.system() == 'Linux':
+    if VIEWMASTER_TESTING:
+        VIEWMASTER_MEMCACHE_PORT = 0
+        PDSFILE_MEMCACHE_PORT = 0
+    else:
+        VIEWMASTER_MEMCACHE_PORT = '/var/run/memcached/memcached.socket'
+        PDSFILE_MEMCACHE_PORT = '/var/run/memcached/memcached.socket'

Also applies to: 40-46


24-31: Re-evaluate MAKE_SYMLINKS defaults.

If symlink creation is removed from startup, keeping MAKE_SYMLINKS=True in production may be misleading.

viewmaster/viewmaster.py (10)

31-33: Do not hard‑code Flask secret keys.

Load from an environment variable and fail closed in non‑testing; hard‑coded secrets are a security risk.

-app = Flask(__name__)
-app.secret_key = "Cassini Grand Finale!"    # needed by flask_wtf
+app = Flask(__name__)
+# Load secret from env; in testing allow a default, but never check in real keys
+app.config['SECRET_KEY'] = os.getenv('VIEWMASTER_SECRET_KEY', 'dev-only')

1-1: Remove duplicate import.

redirect is imported twice.

-from flask import Flask, flash, redirect, render_template, redirect, request, send_file
+from flask import Flask, flash, redirect, render_template, request, send_file

360-363: Remove stray print.

Debug print leaks noise in logs/STDOUT.

-    LOGGER.replace_root(HOLDINGS_PATHS)
-    print(VIEWMASTER_PREFIX_+ICON_URL_)
+    LOGGER.replace_root(HOLDINGS_PATHS)

933-936: Replace cgi.escape with html.escape.

cgi.escape is removed in modern Python; this will break at runtime.

-import cgi
+import html
@@
-    if params['selection']:
-        anchor_suffix = '#' + cgi.escape(params['selection'], quote=True)
+    if params['selection']:
+        anchor_suffix = '#' + html.escape(params['selection'], quote=True)
@@
-    if params['selection']:
-        selection_escaped = cgi.escape(params['selection'], quote=True)
+    if params['selection']:
+        selection_escaped = html.escape(params['selection'], quote=True)

Also applies to: 1779-1781, 1791-1793, 2151-2170


1818-1820: Fix ValidationError import and usage.

Avoid NameError by importing the symbol directly.

-from wtforms import StringField, HiddenField
+from wtforms import StringField, HiddenField
+from wtforms.validators import ValidationError
@@
-    if matchobj is None:
-        raise wtforms.validators.ValidationError("")
+    if matchobj is None:
+        raise ValidationError("")

Also applies to: 1804-1816


1599-1605: Bug: misspelled key and wrong return value.

Fix keuy typo and return the cleaned new_params.

-    for key in defaults:
-        if key not in new_params:
-            new_params[key] = defaults[keuy]
-
-    return clean_query_params(params)
+    for key in defaults:
+        if key not in new_params:
+            new_params[key] = defaults[key]
+
+    return clean_query_params(new_params)

560-566: Undefined variable row_pdsfile.

This branch currently crashes; copy the current query file instead.

-        row_pdsfile = row_pdsfile.copy()
+        row_pdsfile = query_pdsfile.copy()

1844-1851: Path traversal risk in test routes; gate by testing flag.

Routes serve arbitrary filesystem paths without confinement. Restrict to a directory via send_from_directory and only register in testing.

-from flask import ... , send_file
+from flask import ... , send_from_directory
@@
-@app.route('/icons-local/<path:query_path>')
-def return_icons_local(query_path):
-    return send_file(f'../icons/{query_path}', mimetype='image/png')
+if VIEWMASTER_TESTING:
+    @app.route('/icons-local/<path:query_path>')
+    def return_icons_local(query_path):
+        return send_from_directory('../icons', query_path, mimetype='image/png')
@@
-@app.route('/holdings/<path:query_path>')
-def return_holdings_local(query_path):
-    return send_file(f'{HOLDINGS_PATHS[0]}/{query_path}', mimetype='text/plain')
+    @app.route('/holdings/<path:query_path>')
+    def return_holdings_local(query_path):
+        return send_from_directory(HOLDINGS_PATHS[0], query_path)

Also applies to: 1848-1851


1714-1716: Duplicate FILTER_REGEX definitions.

There are two different FILTER_REGEX patterns. Consolidate to one to avoid confusion.

Also applies to: 1802-1804


305-313: Single holdings path assertion.

You assert a single holdings path. If future configs need multiple paths, this will break. Consider logging a warning instead.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a1d29aa and cda1e38.

📒 Files selected for processing (4)
  • viewmaster/doc/viewmaster_v1.md (1 hunks)
  • viewmaster/viewmaster-without-pause.py (4 hunks)
  • viewmaster/viewmaster.py (1 hunks)
  • viewmaster/viewmaster_config.py (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
viewmaster/viewmaster.py (1)
viewmaster/viewmaster-without-pause.py (1)
  • get_holdings_paths (139-146)
🪛 markdownlint-cli2 (0.18.1)
viewmaster/doc/viewmaster_v1.md

1-1: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


2-2: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Above

(MD022, blanks-around-headings)


8-8: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


8-8: Multiple headings with the same content

(MD024, no-duplicate-heading)


9-9: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Above

(MD022, blanks-around-headings)


9-9: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


10-10: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Above

(MD022, blanks-around-headings)


10-10: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


12-12: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


14-14: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


15-15: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


16-16: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


19-19: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


19-19: Multiple headings with the same content

(MD024, no-duplicate-heading)


20-20: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Above

(MD022, blanks-around-headings)


20-20: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


22-22: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)

🔇 Additional comments (1)
viewmaster/viewmaster.py (1)

1897-1905: Pause/resume caching commented out.

Confirm updated Pds3File caching semantics. Consider removing or updating to the new API to avoid stale caches.

Also applies to: 1976-1979

- `python -m venv myenv` (Replace `myenv` with the desired name for your virtual environment)
- `source myenv/bin/activate`
- `pip install -r requirements.txt`
- Create an environement variable `PDS3_HOLDINGS` to store the path of PDS3 holdings.
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Typos and requirement clarity.

  • “environement” → “environment”.
  • Clarify the path must end with “/holdings” to pass validation.
🤖 Prompt for AI Agents
In viewmaster/doc/viewmaster_v1.md around line 17, fix the typo and clarify the
requirement: change “environement” to “environment” and update the sentence to
state that the PDS3_HOLDINGS environment variable should store the path to the
PDS3 holdings and that the path must end with “/holdings” to pass validation.

Comment on lines 21 to 23
- Execute viewmaster.py in the terminal:
- `sudo -E python viewmaster/viewmaster.py`
- Open your browser and enter `http://127.0.0.1:8080/` (this is the value assigned to VIEWMASTER_PREFIX_ in viewmaster_config.py). Now the testing viewmaster will be running on your browser.
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

❓ Verification inconclusive

Avoid sudo for local runs.

Port 8080 doesn’t require root; recommending sudo -E is unnecessary and risky. Use the regular interpreter; if env propagation is needed, export before running.


Remove sudo from local run instructions (viewmaster/doc/viewmaster_v1.md:21-23)
Use python viewmaster/viewmaster.py instead of sudo -E python…; export any needed environment variables beforehand.

🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

22-22: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)

🤖 Prompt for AI Agents
In viewmaster/doc/viewmaster_v1.md around lines 21 to 23, remove the use of sudo
in the local run instructions: replace `sudo -E python viewmaster/viewmaster.py`
with `python viewmaster/viewmaster.py` and add a short note to export any
required environment variables beforehand (e.g., `export VAR=value`) so users
run the script without elevated privileges.

Comment on lines 10 to 12
# Test for a command line run, sudo python viewmaster/viewmaster.py
VIEWMASTER_TESTING = 'viewmaster.py' in sys.argv[0]

Copy link

@coderabbitai coderabbitai bot Sep 29, 2025

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Make testing mode explicit and less brittle.

Relying on 'viewmaster.py' in sys.argv[0] is fragile. Consider an env var flag and/or CLI option.

-VIEWMASTER_TESTING = 'viewmaster.py' in sys.argv[0]
+VIEWMASTER_TESTING = os.getenv('VIEWMASTER_TESTING', '0') == '1'

Committable suggestion skipped: line range outside the PR's diff.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Leave as-is, this is for manually running viewmaster script in the command line during development

Copy link

Choose a reason for hiding this comment

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

@juzen2003, understood! I'll leave the testing detection as-is for your command-line development workflow.

(„• ֊ •„)
づ♡

…up instructions, adding an overview of key components, and detailing the core Python files and their functionalities. Update version information to October 2025.
…p instructions, and local running guidelines for Viewmaster.
…outlining the iterator classes, their purposes, key features, and design patterns for navigating PDS file structures.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cda1e38 and c3386e2.

📒 Files selected for processing (2)
  • README.md (1 hunks)
  • viewmaster/doc/viewmaster_v1.md (1 hunks)
🧰 Additional context used
🪛 LanguageTool
README.md

[style] ~3-~3: Using many exclamation marks might seem excessive (in this case: 3 exclamation marks for a text that’s 1169 characters long)
Context: ...--- | ------------ | -------------- | | PyPI version | Build status | [![Code coverage](https://img.shields.io/c...

(EN_EXCESSIVE_EXCLAMATION)

🪛 markdownlint-cli2 (0.18.1)
viewmaster/doc/viewmaster_v1.md

19-19: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


24-24: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


37-37: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


49-49: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


59-59: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


64-64: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


67-67: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


78-78: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


131-131: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


134-134: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


155-155: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


158-158: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


180-180: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


183-183: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


196-196: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


199-199: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


214-214: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


217-217: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


219-219: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


220-220: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


221-221: Unordered list indentation
Expected: 4; Actual: 8

(MD007, ul-indent)


222-222: Unordered list indentation
Expected: 4; Actual: 8

(MD007, ul-indent)


223-223: Unordered list indentation
Expected: 4; Actual: 8

(MD007, ul-indent)


224-224: Unordered list indentation
Expected: 4; Actual: 8

(MD007, ul-indent)


225-225: Unordered list indentation
Expected: 6; Actual: 12

(MD007, ul-indent)


226-226: Unordered list indentation
Expected: 6; Actual: 12

(MD007, ul-indent)


227-227: Unordered list indentation
Expected: 4; Actual: 8

(MD007, ul-indent)


230-230: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


231-231: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


232-232: Unordered list indentation
Expected: 4; Actual: 8

(MD007, ul-indent)


233-233: Unordered list indentation
Expected: 4; Actual: 8

(MD007, ul-indent)


234-234: Unordered list indentation
Expected: 4; Actual: 8

(MD007, ul-indent)


235-235: Unordered list indentation
Expected: 4; Actual: 8

(MD007, ul-indent)


236-236: Unordered list indentation
Expected: 4; Actual: 8

(MD007, ul-indent)


237-237: Unordered list indentation
Expected: 6; Actual: 12

(MD007, ul-indent)


238-238: Unordered list indentation
Expected: 6; Actual: 12

(MD007, ul-indent)


241-241: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


242-242: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


243-243: Unordered list indentation
Expected: 4; Actual: 8

(MD007, ul-indent)


244-244: Unordered list indentation
Expected: 4; Actual: 8

(MD007, ul-indent)


245-245: Unordered list indentation
Expected: 4; Actual: 8

(MD007, ul-indent)


248-248: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)

README.md

1-1: First line in a file should be a top-level heading

(MD041, first-line-heading, first-line-h1)


18-18: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


23-23: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


36-36: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)

🔇 Additional comments (1)
README.md (1)

1-43: Content aligns well with PR objectives.

The documentation correctly reflects the shift from Apache/httpd configuration parsing to environment-variable-based holdings path management (PDS3_HOLDINGS), and removes references to symlink creation. The setup and local-run instructions are clear and actionable.

Comment on lines 1 to 45
# Viewmaster Help Guide

Version 1.0
October 2025

# Viewmaster

## Overview

**Viewmaster** is a web-based file and document viewer and management tool. It is focused on handling and displaying various types of PDS documents and folders.

---

# Running Viewmaster

## Environment Setup (first-time only)

1. Clone the repository:
```bash
git clone https://github.com/SETI/rms-viewmaster.git
```

2. Create a virtual environment and install dependencies in the `rms-viewmaster` directory:
```bash
python -m venv myenv # Replace "myenv" with your preferred name
source myenv/bin/activate
pip install -r requirements.txt
```

3. Set the environment variable `PDS3_HOLDINGS` to the path of your PDS3 holdings.

---

## Running Locally

1. Start the server:
```bash
sudo -E python viewmaster/viewmaster.py
```

2. Open your browser and go to:
[http://127.0.0.1:8080/](http://127.0.0.1:8080/)
*(This corresponds to `VIEWMASTER_PREFIX_` in `viewmaster_config.py`.)*

---
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

Fix markdown lint violations in setup section.

The document contains multiple linting violations flagged in static analysis and a previous review. Key issues include missing blank lines around headings (MD022), incorrect code block spacing (MD031), and list indentation inconsistencies (MD007). These should be corrected throughout the document, particularly in the setup and running sections.

For example:

  • Add blank lines before/after heading on line 1 and line 6
  • Add blank lines before/after code blocks on lines 19, 24, 37
  • Verify list indentation aligns with markdown standards

Generate a script to identify and fix the remaining markdown lint issues:


🏁 Script executed:

#!/bin/bash
# Description: Run markdownlint on the file to identify remaining issues

markdownlint-cli2 viewmaster/doc/viewmaster_v1.md

Length of output: 12709


Comprehensive markdown linting fixes required throughout the document.

The markdownlint scan identified 103 errors spanning the entire document, not just the setup sections. Address the following violations:

  • MD025 (multiple h1 headings): Lines 6, 14, 47, 74 — restructure to single top-level heading
  • MD031 (blanks around code blocks): Lines 19, 24, 37 — add blank lines before/after fenced code blocks
  • MD022 (blanks around headings): Lines 49, 59, 64, 67, 78, 131, 134, 155, 158, 180, 183, 196, 199, 214, 217 — add blank lines before/after headings
  • MD007 (list indentation): Lines 219–227, 230–238, 241–245, 248 — fix indentation to 2 spaces per level
  • MD013 (line length): Many lines exceed 80 character limit — wrap or reformat
  • MD032 (blanks around lists): Lines 50, 60, 65, 68, 135, 159, 184, 200, 218 — add blank lines before/after lists
  • MD024 (duplicate headings): Multiple instances of "Overview," "Key Components" — use unique headings
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

19-19: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


24-24: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


37-37: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)

🤖 Prompt for AI Agents
In viewmaster/doc/viewmaster_v1.md around lines 1–45, the document fails many
markdownlint rules (MD025, MD031, MD022, MD007, MD013, MD032, MD024) —
consolidate multiple H1s into a single top-level heading, ensure single blank
lines before and after all headings and fenced code blocks, add blank lines
around lists, normalize list indentation to 2 spaces per level, wrap or reflow
long lines to keep under ~80 characters, and rename or merge duplicate section
headings (e.g., "Overview", "Key Components") so each heading is unique; apply
these changes consistently across the file to resolve the reported lint
violations.

pip install -r requirements.txt
```

3. Set the environment variable `PDS3_HOLDINGS` to the path of your PDS3 holdings.
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Clarify that PDS3_HOLDINGS path must end with /holdings.

The instruction should explicitly state that the path must end with /holdings to pass validation. This requirement was noted in a previous review and should be included for clarity.

-3. Set the environment variable `PDS3_HOLDINGS` to the path of your PDS3 holdings.
+3. Set the environment variable `PDS3_HOLDINGS` to the absolute path of your PDS3 holdings directory (must end with `/holdings`):
+
+   ```bash
+   export PDS3_HOLDINGS=/absolute/path/to/holdings
+   ```
🤖 Prompt for AI Agents
In viewmaster/doc/viewmaster_v1.md around line 30, clarify that the
PDS3_HOLDINGS environment variable must point to the absolute path that ends
with "/holdings" so validation will pass; update the text to explicitly state
the trailing "/holdings" requirement and adjust the example export line to show
an absolute path ending with "/holdings".


1. Start the server:
```bash
sudo -E python viewmaster/viewmaster.py
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

CRITICAL: Remove sudo -E from local run instructions.

Port 8080 does not require root privileges. Using sudo -E is unnecessary and introduces a security risk. This issue was flagged in a previous review and should be corrected before merge.

  1. Start the server:
     ```bash
-    sudo -E python viewmaster/viewmaster.py
+    python viewmaster/viewmaster.py
     ```

If environment variable propagation is needed, export variables before executing:

export PDS3_HOLDINGS=/path/to/holdings
python viewmaster/viewmaster.py

I'll verify the claims in this review comment by examining the file and the underlying code.


---

🏁 Script executed:

```shell
#!/bin/bash
# Examine the file content around line 38
cat viewmaster/doc/viewmaster_v1.md | head -50 | tail -20

Length of output: 357


🏁 Script executed:

#!/bin/bash
# Search for port configuration in viewmaster.py
rg -n "port|8080|listen" viewmaster/viewmaster.py -C2

Length of output: 1497


🏁 Script executed:

#!/bin/bash
# Check if viewmaster.py requires elevated privileges or uses ports < 1024
rg -n "sudo|root|privilege|:80\b|:443\b|:8080" viewmaster/viewmaster.py

Length of output: 530



---

🏁 Script executed:

```shell
#!/bin/bash
# Check for file system operations that might require elevated privileges
rg -n "open\(|mkdir|symlink|chmod|chown|mount|rmtree" viewmaster/viewmaster.py -C1

Length of output: 2577


🏁 Script executed:

#!/bin/bash
# Check viewmaster_config.py to see what environment setup is needed
cat viewmaster/viewmaster_config.py | head -100

Length of output: 1802


🏁 Script executed:

#!/bin/bash
# Look for any explicit privilege checks or warnings in the documentation
rg -n "sudo|root|privilege|elevation" viewmaster/doc/viewmaster_v1.md

Length of output: 45


CRITICAL: Remove sudo -E from local run instructions.

Port 8080 does not require root privileges. Using sudo -E is unnecessary and introduces a security risk. This issue was flagged in a previous review and should be corrected before merge.

  1. Start the server:
     ```bash
-    sudo -E python viewmaster/viewmaster.py
+    python viewmaster/viewmaster.py
     ```

If environment variable propagation is needed, export variables before executing:

export PDS3_HOLDINGS=/path/to/holdings
python viewmaster/viewmaster.py
🤖 Prompt for AI Agents
In viewmaster/doc/viewmaster_v1.md around line 38, the local run instruction
uses "sudo -E python viewmaster/viewmaster.py" which is unnecessary and
insecure; remove "sudo -E" so the command runs as "python
viewmaster/viewmaster.py" and update the surrounding guidance to show exporting
any required environment variables beforehand (e.g., include a short note or
example showing export PDS3_HOLDINGS=... followed by running the python
command).

Comment on lines 47 to 256
# Key Components

## 1. Main Application Code (`viewmaster/`)
- **Python Modules:**
- `link.py`, `pdsgroup.py`, `pdsgrouptable.py`, `pdsiterator.py`: Handle core logic for grouping, iterating, and linking PDS documents or datasets.
- `viewmaster.py`, `viewmaster-without-pause.py`: Main entry points and application logic for the viewer.
- `viewmaster_config.py`: Configuration settings for the application.

- **Templates (`viewmaster/templates/`):**
- HTML files for rendering views: tables, grids, navigation, error pages, feedback, etc.
- These templates provide the web interface, including navigation, table/grid views, and feedback/error handling.

## 2. Static Assets (`icons/`)
- Organized by color (black, blue, gray) and size (30, 50, 100, 200, 500).
- Icons for document types (archive, binary, book, image, PDF, software, etc.) and folders (archives, binary, cubes, diagrams, etc.).
- Used for visually representing files and folders in the UI.

## 3. Web Server Integration
- `viewmaster.wsgi`, `link.wsgi`, `wsgi_init.py`: WSGI entry points for deploying the application with a WSGI-compatible web server.

## 4. Project Metadata
- `README.md`: Project overview and instructions.
- `requirements.txt`: Python dependencies.
- `LICENSE`, `CODE_OF_CONDUCT.md`, `CONTRIBUTING.md`: Standard open-source project files.

---

# Summary of the Core Python Files

## `viewmaster.py` and `viewmaster-without-pause.py`

### Overview
`viewmaster.py` is the core module of the application. It sets up a Flask-based server that serves as a document viewer for PDS files. Users can browse, view, and navigate through a hierarchical structure of files and folders, with support for multiple document types and metadata.

`viewmaster-without-pause.py` is a variant designed to run without pausing or waiting for user input.

### Key Components and Functionality

1. **Imports and Initialization**
- Imports Flask and related modules (FlaskForm, StringField, HiddenField) for web handling and form processing.
- Imports utility modules (os, sys, logging, psutil, etc.) and custom modules (pdsfile, pdsiterator, pdslogger, pdstable, pdsgroup, pdsgrouptable).
- Initializes the Flask app with a secret key and configuration values (like `LOCAL_IP_ADDRESS`).
- Imports configuration parameters (e.g., `LOCALHOST_`, `VIEWMASTER_PREFIX_`, `WEBSITE_HTTP_HOME`) from `viewmaster_config.py`.
- Configures logging with `pdslogger`, with separate log files for info and debug levels.

2. **Constants and Configuration**
- Defines constants such as `ICON_ROOT_`, `ICON_URL_`, and `VIEWABLE_EXTENSIONS`.
- Defines `ASSOCIATED_CATEGORIES` for navigation and grouping.
- Sets UI and navigation limits (`MAX_PAGES`, `MAX_NAV_STRLEN`, etc.).

3. **Holdings Path Management**
- `get_holdings_paths()` reads the Apache configuration file (or environment variable) to determine the holdings directories.
- `validate_holdings_paths()` checks for existence and logs warnings/errors.
- `create_holdings_symlinks()` likely creates symbolic links for easier access.

4. **Cache Initialization**
- Functions like `initialize_caches()`, `build_cache()`, and `reset_cache()` manage caching for performance.
- Uses memcached (via `pylibmc`) for caching.

5. **Navigation and Page Generation**
- Functions such as `get_prev_next_navigation()`, `list_next_pdsfiles()`, and `fill_level_navigation_links()` generate navigation links.
- `get_directory_page()` and `directory_page_html()` render directory listings.
- `get_product_page_info()` and `product_page_html()` render product pages.

6. **URL Parameter Handling**
- Functions like `get_query_params_from_request()`, `get_query_params_from_url()`, and `clean_query_params()` parse and sanitize URL parameters.
- `url_params()` constructs URLs with the proper parameters.

7. **Form Handling**
- Defines a `FilterForm` class (Flask-WTF) for file name filtering.
- The `/set_filter` route processes POST requests to update filters.

8. **Route Definitions**
- The main route (`@app.route('/<path:query_path>')`) handles all incoming requests.
- Additional routes handle filtering, serving icons, holdings, and feedback.

9. **Utility Functions**
- Functions like `format_row_value()` and `format_tuple()` prepare data for display.
- `trim_html()` likely cleans up HTML for rendering.

---

## `viewmaster_config.py`

### Overview
`viewmaster_config.py` is the configuration module for Viewmaster. It defines environment-specific settings (URLs, logging, caching) based on whether the app is in testing, local, or production mode.

### Key Components and Functionality
1. **Environment Detection**
- Uses `platform` and `socket` to detect OS and hostname.
- Flags like `VIEWMASTER_TESTING` and `VIEWMASTER_FOR_MARK` determine the environment.

2. **Configuration Blocks**
- **Testing**: Localhost URLs, caching disabled, log name `pds.viewmaster.testing`.
- **Local**: Custom localhost URLs, symlinks enabled, caching disabled, log name `pds.viewmaster.local`.
- **Production**: Deployment URLs, symlinks enabled, caching disabled, log name `pds.viewmaster.server`.

3. **Platform-Specific Settings**
- Linux: uses `/var/run/memcached/memcached.socket`, `/var/www/` paths, Apache config in `/etc/apache2/`.
- macOS: uses `/var/tmp/memcached.socket`, `/Library/WebServer/` paths, Apache config in `/usr/local/etc/httpd/`.

4. **Other Settings**
- `USE_SHELVES_ONLY = False` (application supports multiple storage mechanisms).

---

## `link.py`

### Overview
`link.py` is a Flask-based microservice for URL redirection. It redirects requests either to the main Viewmaster app (for directories) or directly to files.

### Key Components and Functionality
1. **Imports and Initialization**
- Uses Flask (redirect, abort), os, glob, logging, and `pdslogger`.
- Imports configuration from `viewmaster_config.py`.

2. **Logging Configuration**
- Log name derived from Viewmaster config, replacing "viewmaster" with "link".
- Configured for rotating log files.

3. **Route Definitions**
- Main route: `/path:query_path>` defaults to `volumes`.
- Determines whether to redirect to a directory view or directly to a file.

4. **Redirection Logic**
- If a directory: redirects to `VIEWMASTER_PREFIX_ + query_path`.
- If a file: finds file via glob, then redirects to `WEBSITE_HTTP_HOME + relative path`.
- If missing: logs error and returns 404.

---

## `pdsgroup.py`

### Overview
`pdsgroup.py` defines the `PdsGroup` class for managing an ordered set of `PdsFile` objects. Groups related files (e.g., a file and its label) for display in the UI.

### Key Components
- **Initialization**: Stores parent directory, anchor, and files.
- **Methods**:
- `__len__`, `__repr__`, `copy()`.
- Sorting (`sort()`), grouping (`group_children()`).
- File management (`append()`, `remove()`, `hide()`, `unhide()`).
- Iterators for visible/hidden/all files.
- **Icons/Viewsets**: Manages closed/open states and viewable sets.

---

## `pdsgrouptable.py`

### Overview
`pdsgrouptable.py` defines the `PdsGroupTable` class for managing an ordered set of `PdsGroup` objects. It organizes groups into tables for display.

### Key Components
- **Initialization**: Stores groups and parent.
- **Methods**:
- `__repr__`, `copy()`.
- Insert groups/files (`insert_group`, `insert_file`, `insert()`).
- Sorting (`sort_in_groups`, `sort_groups`).
- File/group hiding/removal.
- Iterators for groups/files.
- **Static Methods**:
- `sort_tables()`, `tables_from_pdsfiles()`, `merge_index_row_tables()`.

---

## `pdsiterator.py`

### Overview
`pdsiterator.py` provides three main iterator classes for navigating through PDS file structures.

### Key Components and Functionality
- **PdsDirIterator** (lines 29-144)
- **Purpose**: Iterates across related directories, jumping into adjacent directories and parallel volumes
- **Key Features**:
- Uses a global `DIRECTORY_CACHE` to optimize performance by caching directory listings
- Supports both forward and backward iteration (via `sign` parameter)
- Handles case-insensitive path matching
- Returns tuples of `(logical_path, display_path, level)` where:
- `level = 0`: same directory level
- `level = 1`: different directory level
- Uses `pdsfile.NEIGHBORS` to find related directories using fnmatch patterns

- **PdsFileIterator** (lines 150-268)
- **Purpose**: Iterates through files within directories, with support for jumping to adjacent directories when needed
- **Key Features**:
- Supports pattern matching and filtering of file names
- Can exclude files based on patterns
- Supports custom filter functions
- Handles "cousin" navigation (jumping to adjacent parent directories)
- Returns tuples with level indicators:
- `level = 0`: sibling files (same directory)
- `level = 1`: cousin files (different directory)

- **PdsRowIterator** (lines 274-336)
- **Purpose**: Simple iterator for files within a single directory (siblings only)
- **Key Features**:
- More lightweight than `PdsFileIterator`
- Only iterates within the same parent directory
- Always returns `level = 0` since it doesn't cross directory boundaries

- **Utility Functions:**
- **`dirs_only()`** (lines 12-16): Filter function that only returns directories

- **Key Design Patterns:**
1. **Caching**: Uses `DIRECTORY_CACHE` to avoid repeated expensive directory operations
2. **Case-insensitive matching**: All path comparisons are done in lowercase
3. **Iterator protocol**: All classes implement `__iter__()`, `__next__()`, and `next()` methods
4. **Cloning**: Each iterator has a `copy()` method for creating reversed or modified versions
5. **Flexible navigation**: Supports both forward (`sign=1`) and backward (`sign=-1`) iteration

Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Extended documentation is comprehensive but contains formatting issues.

The detailed breakdown of Key Components, Core Python Files, and module documentation (lines 47-256) is thorough and helpful. However, static analysis flags multiple list indentation issues (MD007) throughout these sections. Consider running a markdown linter to fix all indentation to the standard 2-space increment and ensure blank lines separate headings as per MD022.

🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

49-49: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


59-59: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


64-64: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


67-67: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


78-78: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


131-131: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


134-134: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


155-155: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


158-158: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


180-180: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


183-183: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


196-196: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


199-199: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


214-214: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


217-217: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


219-219: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


220-220: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


221-221: Unordered list indentation
Expected: 4; Actual: 8

(MD007, ul-indent)


222-222: Unordered list indentation
Expected: 4; Actual: 8

(MD007, ul-indent)


223-223: Unordered list indentation
Expected: 4; Actual: 8

(MD007, ul-indent)


224-224: Unordered list indentation
Expected: 4; Actual: 8

(MD007, ul-indent)


225-225: Unordered list indentation
Expected: 6; Actual: 12

(MD007, ul-indent)


226-226: Unordered list indentation
Expected: 6; Actual: 12

(MD007, ul-indent)


227-227: Unordered list indentation
Expected: 4; Actual: 8

(MD007, ul-indent)


230-230: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


231-231: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


232-232: Unordered list indentation
Expected: 4; Actual: 8

(MD007, ul-indent)


233-233: Unordered list indentation
Expected: 4; Actual: 8

(MD007, ul-indent)


234-234: Unordered list indentation
Expected: 4; Actual: 8

(MD007, ul-indent)


235-235: Unordered list indentation
Expected: 4; Actual: 8

(MD007, ul-indent)


236-236: Unordered list indentation
Expected: 4; Actual: 8

(MD007, ul-indent)


237-237: Unordered list indentation
Expected: 6; Actual: 12

(MD007, ul-indent)


238-238: Unordered list indentation
Expected: 6; Actual: 12

(MD007, ul-indent)


241-241: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


242-242: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


243-243: Unordered list indentation
Expected: 4; Actual: 8

(MD007, ul-indent)


244-244: Unordered list indentation
Expected: 4; Actual: 8

(MD007, ul-indent)


245-245: Unordered list indentation
Expected: 4; Actual: 8

(MD007, ul-indent)


248-248: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)

🤖 Prompt for AI Agents
In viewmaster/doc/viewmaster_v1.md around lines 47 to 256 there are multiple
markdown list indentation errors (MD007) and missing blank lines after headings
(MD022); run a markdown linter (e.g., markdownlint) and fix by normalizing list
indentation to 2-space increments for each nesting level, ensure nested bullets
align under their parent items, and add a single blank line after each heading;
reflow any mis-indented blocks so headings are followed by a blank line and
lists consistently use 2-space indentation throughout.

@rfrenchseti
Copy link
Collaborator

A couple of initial comments:

  • Elsewhere we use PDS3_HOLDINGS_DIR as the environment variable; this should be changed to be consistent.
  • All the Markdown issues should be addressed; the file won't display properly in all Markdown viewers otherwise.
  • Claude Code is really bad at writing documentation. I've tried many different ways of getting Cursor to write documentation for existing code, and it's always awful. It comes out in the format you see here - short bullet lists of incomplete information. While this might give you some initial structure, you're going to have to do a lot of work to flesh it out so it's actually useful to a human.

Different LLMs have different skills. Claude is tuned for coding, not for English prose. On the other hand, ChatGPT is excellent at prose. I suggest that you write a bunch of messy documentation yourself, perhaps building on what Claude wrote for you. Don't worry too much about spelling, grammar, or structure. Just get all the details in. Then paste it in to ChatGPT with a prompt like:

"You are an expert English editor, Python programmer, and web developer. Below is technical documentation for a web application. Your job is to edit this document into a fully readable technical description, properly organized with headings and subheadings, with correct English spelling and grammar. The target audience is an experienced Python developer who is not previously familiar with this code base but needs to become an expert. Keep all details in the original document. Also mark areas where there is ambiguity and make suggestions for ways in which the document can be further improved at both the detail and general level. The result should be in Markdown."

@rfrenchseti
Copy link
Collaborator

Also I agree that you shouldn't be using sudo to run Viewmaster. If it's required something is going wrong.

Updated the logger name variable from LOGNAME to link_logger_name to better reflect its purpose. This change enhances code readability and maintains consistency in naming conventions within the logging implementation.
Introduced a module-level logger cache to prevent duplicate logger instances. The new get_or_create_logger function ensures that the logger is created only once, enhancing performance and maintaining consistent logging behavior across requests. Updated the link function to utilize this new caching mechanism.
Introduced a ValueError exception to be raised when neither Pds3File nor Pds4File can create a parent PdsFile from the given logical path. This change improves error reporting and enhances the robustness of the PdsFileIterator class.
Introduced a get_or_create_logger function to ensure the logger instance is created only once, preventing duplicate handlers. Updated the main execution block to utilize this new caching mechanism, enhancing performance and maintaining consistent logging behavior.
Updated instances of logger.warn to logger.warning for consistency and clarity in logging messages throughout the viewmaster.py file. This change enhances the logging functionality and maintains uniformity in the codebase.
… messages

Updated the exception handling in get_holdings_path to log a more descriptive message when failing to get or validate holdings paths. Additionally, modified the Memcache connection error handling to remove the exception variable, streamlining the warning message. These changes enhance the clarity and usefulness of log outputs.
…nd functionality

Updated the handling of HOLDINGS_PATHS and PAGE_CACHE to ensure they are properly initialized and managed. Introduced global declarations for these variables in relevant functions, enhancing the organization of cache management. This change streamlines the caching process and improves code readability.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 11

Caution

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

⚠️ Outside diff range comments (10)
viewmaster/viewmaster_config.py (1)

47-50: Redundant memcache port assignments for testing mode.

Memcache ports are set to 0 in the testing block (lines 47-48), then conditionally set again for non-Linux testing (lines 68-70). This duplication is harmless but could be simplified.

Suggested consolidation
 if VIEWMASTER_TESTING:
     LOCALHOST_ = '/'
     VIEWMASTER_PREFIX_ = 'http://127.0.0.1:8080/'
     WEBSITE_HTTP_HOME = 'http://localhost'
     LOGNAME = 'pds.viewmaster.testing'
-    VIEWMASTER_MEMCACHE_PORT = 0
-    PDSFILE_MEMCACHE_PORT = 0
     MAKE_SYMLINKS = False
     PAGE_CACHING = False
 # ...
 else:
-    if VIEWMASTER_TESTING:
-        VIEWMASTER_MEMCACHE_PORT = 0
-        PDSFILE_MEMCACHE_PORT = 0
-    else:
-        VIEWMASTER_MEMCACHE_PORT = '/var/tmp/memcached.socket'
-        PDSFILE_MEMCACHE_PORT = '/var/tmp/memcached.socket'
+    VIEWMASTER_MEMCACHE_PORT = '/var/tmp/memcached.socket'
+    PDSFILE_MEMCACHE_PORT = '/var/tmp/memcached.socket'

Then set memcache ports to 0 in the VIEWMASTER_TESTING block unconditionally.

Also applies to: 68-73

viewmaster/pdsgroup.py (2)

296-403: Critical: Duplicate sort() method definition.

The sort() method is defined twice (lines 296-348 and lines 350-403). The second definition completely shadows the first. This appears to be a copy-paste error.

Fix: Remove the duplicate method
-    def sort(self, labels_after=None, dirs_first=None, dirs_last=None,
-                   info_first=None):
-        """Sort the rows of this group.
-
-        Parameters:
-            labels_after (bool|None): Place labels after their targets.
-            dirs_first (bool|None): Sort directories before files.
-            dirs_last (bool|None): Sort directories after files.
-            info_first (bool|None): Prioritize info files.
-
-        Returns:
-            None
-        """
-
-        basename_dict = {pdsf.basename:pdsf for pdsf in self.rows}
-
-        if self.parent_pdsf:
-            sorted = self.parent_pdsf.sort_basenames(list(basename_dict.keys()),
-                                                labels_after=labels_after,
-                                                dirs_first=dirs_first,
-                                                dirs_last=dirs_last,
-                                                info_first=info_first)
-
-            # If there are multiple labels in the group, make sure each label
-            # is adjacent to any data files of the same name. This can comes up
-            # with Juno JIRAM, at least.
-            labels = [basename for basename in sorted
-                      if self.parent_pdsf.basename_is_label(basename)]
-            if len(labels) > 1:
-              for label in labels:
-                l = sorted.index(label)                     # index of label
-                pattern = os.path.splitext(label)[0] + '.'
-                tlist = [t for t in range(len(sorted))      # indices of targets
-                         if sorted[t].startswith(pattern)]
-                tlist.remove(l)
-
-                # If the label is not adjacent, but could be...
-                if tlist and (l+1) not in tlist and (l-1) not in tlist:
-                    # Locate the nearest target with a matching basename
-                    diffs = [(abs(t-l),t) for t in tlist]
-                    tbest = min(diffs)[1]
-
-                    # Relocate the label to an adjacent location
-                    if l < tbest:
-                        sorted.insert(tbest - 1, sorted.pop(l))
-                    else:
-                        sorted.insert(tbest + 1, sorted.pop(l))
-
-        else:
-            sorted = list(basename_dict.keys())     # for merged directories
-            sorted.sort()
-
-        self.rows = [basename_dict[key] for key in sorted]
-
     def sort(self, labels_after=None, dirs_first=None, dirs_last=None,
                    info_first=None):

Keep only the second definition (lines 350-403).


33-56: Unused anchor parameter in constructor.

The anchor parameter is documented and accepted but never used—self.anchor is always set to None initially and later derived from the first appended pdsfile. Either use the parameter or remove it.

Fix: Use the anchor parameter
     def __init__(self, pdsfiles=None, parent=False, anchor=None, hidden=None):
         # ...
         self.parent_pdsf = parent
-        self.anchor = None
+        self.anchor = anchor
         self.rows = []
viewmaster/pdsiterator.py (1)

78-90: Critical: Missing return after handling pdsf is None.

When pdsf is None, the code sets initial attributes but then falls through to line 86+ which accesses pdsf.NEIGHBORS, causing an AttributeError.

Fix: Add return statement
         if pdsf is None:
             self.neighbors = []
             self.neighbor_index = 0
             self.current_logical_path = None
             self.sign = 1
+            self.neighbors_lc = []
+            self.logger = logger
+            return

-        if isinstance(pdsf, pdsfile.Pds3File):
+        if isinstance(pdsf, pdsfile.Pds3File):
viewmaster/viewmaster.py (6)

216-353: Don’t sys.exit(1) from library code; raise and let the caller decide.

get_holdings_path() calls sys.exit(1) on failure, which will take down the WSGI worker/process if this path is used in production initialization. Also the comment mentions PDS3_HOLDINGS but the code uses PDS3_HOLDINGS_DIR. \

Proposed patch (raise, and align message)
 def get_holdings_paths():
@@
-    pds3_holdings_dir = os.getenv('PDS3_HOLDINGS_DIR')  # XXX PDS4
+    pds3_holdings_dir = os.getenv('PDS3_HOLDINGS_DIR')  # XXX PDS4
     if pds3_holdings_dir is not None:
         return [pds3_holdings_dir]
     else:
-        raise IOError("'PDS3_HOLDINGS_DIR' environment variable not set")
+        raise OSError("'PDS3_HOLDINGS_DIR' environment variable not set")
@@
 def get_holdings_path(logger):
@@
     try:
         paths = get_holdings_paths()
         paths = validate_holdings_paths(paths, logger)
-    except Exception as e:
+    except Exception:
         logger.exception('Failed to get or validate holdings path')
-        sys.exit(1)
+        raise

541-654: Fix row_pdsfile NameError for missing index-row navigation.

In the if query_pdsfile.is_index_row and not query_pdsfile.exists: block, row_pdsfile is referenced but never defined, so this path will crash. \

Proposed patch
     if query_pdsfile.is_index_row and not query_pdsfile.exists:
@@
-        row_pdsfile = row_pdsfile.copy()
+        row_pdsfile = query_copy.copy()
         row_pdsfile.nav_name = row_pdsfile.basename
         row_pdsfile.division = False
         row_pdsfile.terminated = False

1765-1801: Fix dict-iteration deletion + wrong return in get_query_params_from_dict().

You delete keys while iterating new_params (raises RuntimeError) and then return clean_query_params(params) instead of the normalized dict. \

Proposed patch
 def get_query_params_from_dict(params):
@@
-    # Remove unrecognized keys
-    new_params = params.copy()
-    for key in new_params:
-        if key not in defaults:
-            del new_params[key]
+    # Remove unrecognized keys
+    new_params = {k: v for k, v in params.items() if k in defaults}
@@
     for key in defaults:
         if key not in new_params:
             new_params[key] = defaults[key]
 
-    return clean_query_params(params)
+    return clean_query_params(new_params)

2347-2409: Declare global PAGE_CACHE in fill_page_cache() (assignment currently local-only).

PAGE_CACHE = get_page_cache(logger) creates a local variable and won’t update the module global. \

Proposed patch
 def fill_page_cache(logger):
@@
+    global PAGE_CACHE
@@
-    PAGE_CACHE = get_page_cache(logger)
+    PAGE_CACHE = get_page_cache(logger)

2142-2267: Ensure LOGGER and PAGE_CACHE are initialized on first request.

viewmaster() uses LOGGER and PAGE_CACHE directly (e.g., LOGGER.info(), LOGGER.exception(), PAGE_CACHE[key]) without calling get_or_create_logger() or checking for initialization. When running under WSGI, the __main__ block never executes, leaving these globals as None. This crashes on the first request with AttributeError: 'NoneType' object has no attribute 'info'.

Minimal patch (lazy initialization on first request)
 def viewmaster(query_path):
@@
-    global LOGGER, PAGE_CACHE
+    global LOGGER, HOLDINGS_PATHS, PAGE_CACHE
+
+    if LOGGER is None:
+        LOGGER = get_or_create_logger()
+    if HOLDINGS_PATHS is None:
+        HOLDINGS_PATHS = get_holdings_path(LOGGER)
+    if PAGE_CACHE is None:
+        PAGE_CACHE = get_page_cache(LOGGER)

1043-1077: Replace deprecated cgi.escape with html.escape (Python compatibility).

cgi.escape has been removed in Python 3.11+ and will cause ImportError at runtime. This must be replaced with html.escape, which has an identical signature including the quote=True parameter.

Two usages require updating:

  • Line 1074: anchor_suffix = '#' + cgi.escape(params['selection'], quote=True)
  • Line 2007: selection_escaped = cgi.escape(params['selection'], quote=True)

Add import html to the imports and replace both instances.

Proposed patch
+import html
@@
-        anchor_suffix = '#' + cgi.escape(params['selection'], quote=True)
+        anchor_suffix = '#' + html.escape(params['selection'], quote=True)
@@
-        selection_escaped = cgi.escape(params['selection'], quote=True)
+        selection_escaped = html.escape(params['selection'], quote=True)
🤖 Fix all issues with AI agents
In @README.md:
- Around line 32-37: The README currently instructs creating /Library/WebServer
with sudo; change this to avoid requiring elevated privileges by updating the
docs and the app to use a user-writable default and an environment variable
override: replace the hardcoded /Library/WebServer recommendation with a
user-local default (e.g., ~/.viewmaster or a repo-local ./viewmaster_data) and
document an env var like VIEWMASTER_WEBROOT (or similar) that the application
reads to set the webroot; update README to show creating a user-writable
directory (no sudo) and how to set VIEWMASTER_WEBROOT, and change any code that
assumes /Library/WebServer to read the new env var and fall back to the
user-local default.

In @viewmaster/link.py:
- Around line 45-96: Extract the duplicated logger setup into a shared helper
(e.g., a new function in viewmaster/logging_utils.py) that encapsulates creating
a pdslogger.PdsLogger, adding the file handler with rotation, handling
OSError/IOError, and performing the two blankline/info startup call; then
replace Link's create_logger() to call that helper (passing the LOGNAME
replacement 'viewmaster'->'link', LOG_ROOT_PREFIX_+'link.log', limits and pid
flags) and keep get_or_create_logger() as a thin cached wrapper that delegates
to the shared helper; ensure unique symbols to update are create_logger,
get_or_create_logger in viewmaster/link.py and the new exported helper name
(e.g., setup_pds_logger) in the shared module.
- Around line 67-68: The except clause currently catches the same exception
twice ("except (OSError, IOError) as e") — simplify it by removing the redundant
alias and catch only OSError; update the exception handler around the
logger.warning call (the except block that references info_logfile and
logger.warning) to use "except OSError as e" so the code is clearer and avoids
duplicate exception types.

In @viewmaster/pdsgroup.py:
- Around line 538-544: Replace the explicit list comprehension in
iterator_for_all with a direct list conversion for clarity and performance:
change the return in PdsGroup.iterator_for_all from using "[r for r in
self.rows]" to using "list(self.rows)" so it simply returns list(self.rows).

In @viewmaster/pdsiterator.py:
- Around line 311-317: In PdsFileIterator.copy() the for-loop trying
Pds3File.from_logical_path and Pds4File.from_logical_path can leave pdsf
undefined if both raise ValueError; add an else: on the for-loop that raises a
clear ValueError (or RuntimeError) mentioning the invalid
self.current_logical_path so the method fails fast, then proceed to construct
PdsFileIterator(pdsf, sign=sign1, ...) as before; reference the loop over
pdsfile.Pds3File and pdsfile.Pds4File and the variable pdsf when adding this
guard.
- Around line 155-161: The loop over pdsfile.Pds3File and pdsfile.Pds4File can
exit without defining pdsf if both from_logical_path calls raise ValueError;
update the loop to use a for/else pattern like next_cousin: try
from_logical_path on each class_name, break on success, and add an else that
raises a clear ValueError (or logs and raises) when neither succeeds so
PdsDirIterator(pdsf, ...) never receives an unbound pdsf; reference the symbols
pdsfile.Pds3File, pdsfile.Pds4File, from_logical_path, and PdsDirIterator when
making the change.

In @viewmaster/viewmaster_config.py:
- Around line 38-39: Replace the fragile filename check for VIEWMASTER_TESTING
with an explicit environment-variable-based toggle: check
os.environ.get('VIEWMASTER_TESTING') (e.g. equals '1' or a truthy value) to
determine testing mode, falling back to the existing
sys.argv[0].endswith('viewmaster.py') only if the env var is not set; update the
VIEWMASTER_TESTING assignment and add an import for os if missing so the new
env-var check is used by the module.

In @viewmaster/viewmaster.py:
- Around line 2106-2125: The return_holdings_local route builds file_path by
concatenating HOLDINGS_PATHS[0] and query_path which allows path traversal;
replace this with a safe serve-from-directory approach: use Flask's
send_from_directory(HOLDINGS_PATHS[0], query_path, mimetype=...) or explicitly
resolve and validate the realpath of the joined path (os.path.realpath) and
ensure it startswith the holdings root before calling send_file; update
return_holdings_local to perform this safe-join/validation and
return_from_directory/send_file only after confirming the target is inside
HOLDINGS_PATHS[0].
- Around line 355-400: get_page_cache currently reassigns
VIEWMASTER_MEMCACHE_PORT inside the function causing an UnboundLocalError;
instead, capture the global value into a local variable (e.g., memcache_port =
VIEWMASTER_MEMCACHE_PORT) at the start of get_page_cache, use memcache_port for
the conditional checks and when creating pdscache.MemcachedCache, and set
memcache_port = 0 on failure (inside the except) rather than mutating the
global; leave PAGE_CACHE handling as-is and do not add a global declaration for
VIEWMASTER_MEMCACHE_PORT.
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8c30488 and 8c13fb8.

📒 Files selected for processing (6)
  • README.md
  • viewmaster/link.py
  • viewmaster/pdsgroup.py
  • viewmaster/pdsiterator.py
  • viewmaster/viewmaster.py
  • viewmaster/viewmaster_config.py
🧰 Additional context used
🧠 Learnings (8)
📓 Common learnings
Learnt from: juzen2003
Repo: SETI/rms-viewmaster PR: 14
File: README.md:16-18
Timestamp: 2026-01-12T21:03:16.187Z
Learning: In the rms-viewmaster repository, the README.md format should match the README format used in other SETI repositories (such as rms-starcat) for consistency, even if it results in MD031 linting violations (missing blank lines around fenced code blocks).
Learnt from: juzen2003
Repo: SETI/rms-viewmaster PR: 14
File: viewmaster/pdsgrouptable.py:21-22
Timestamp: 2025-12-18T21:42:57.270Z
Learning: In viewmaster, use `pdsfile.Pds3File` explicitly in code implementation instead of the generic `pdsfile.PdsFile` base class in pdsgrouptable.py and pdsiterator.py. Docstrings should reference `Pds3File`, with `Pds4File` to be added later when PDS4 is fully supported.
📚 Learning: 2026-01-12T21:03:16.187Z
Learnt from: juzen2003
Repo: SETI/rms-viewmaster PR: 14
File: README.md:16-18
Timestamp: 2026-01-12T21:03:16.187Z
Learning: In the rms-viewmaster repository, the README.md format should match the README format used in other SETI repositories (such as rms-starcat) for consistency, even if it results in MD031 linting violations (missing blank lines around fenced code blocks).

Applied to files:

  • README.md
  • viewmaster/viewmaster_config.py
  • viewmaster/pdsiterator.py
  • viewmaster/viewmaster.py
📚 Learning: 2025-12-12T23:26:32.767Z
Learnt from: juzen2003
Repo: SETI/rms-viewmaster PR: 14
File: docs/Makefile:11-20
Timestamp: 2025-12-12T23:26:32.767Z
Learning: In the rms-viewmaster repository, the `docs/Makefile` is auto-generated by Sphinx and should not be modified, even if linters flag missing standard phony targets.

Applied to files:

  • README.md
  • viewmaster/viewmaster.py
📚 Learning: 2026-01-12T22:07:33.958Z
Learnt from: juzen2003
Repo: SETI/rms-viewmaster PR: 14
File: viewmaster/viewmaster.py:2496-2501
Timestamp: 2026-01-12T22:07:33.958Z
Learning: In the rms-viewmaster repository, the `if __name__ == "__main__":` block in `viewmaster/viewmaster.py` with `debug=True` is intentionally kept for development purposes only. This code path is not executed in production, which uses a different deployment method (e.g., WSGI server).

Applied to files:

  • README.md
  • viewmaster/viewmaster_config.py
📚 Learning: 2025-12-18T21:42:57.270Z
Learnt from: juzen2003
Repo: SETI/rms-viewmaster PR: 14
File: viewmaster/pdsgrouptable.py:21-22
Timestamp: 2025-12-18T21:42:57.270Z
Learning: In viewmaster, use `pdsfile.Pds3File` explicitly in code implementation instead of the generic `pdsfile.PdsFile` base class in pdsgrouptable.py and pdsiterator.py. Docstrings should reference `Pds3File`, with `Pds4File` to be added later when PDS4 is fully supported.

Applied to files:

  • viewmaster/pdsgroup.py
  • viewmaster/viewmaster.py
📚 Learning: 2025-12-18T21:42:45.915Z
Learnt from: juzen2003
Repo: SETI/rms-viewmaster PR: 14
File: viewmaster/pdsgrouptable.py:21-22
Timestamp: 2025-12-18T21:42:45.915Z
Learning: In viewmaster/pdsgrouptable.py and viewmaster/pdsiterator.py, implement and reference Pds3File explicitly instead of using the generic PdsFile base class. Update docstrings to reference Pds3File, with Pds4File to be introduced later when PDS4 is fully supported. This guidance should be reflected in imports, instantiations, and type hints to improve specificity and future maintainability.

Applied to files:

  • viewmaster/pdsiterator.py
📚 Learning: 2026-01-12T21:13:15.917Z
Learnt from: juzen2003
Repo: SETI/rms-viewmaster PR: 14
File: viewmaster/pdsgrouptable.py:460-467
Timestamp: 2026-01-12T21:13:15.917Z
Learning: In viewmaster's pdsgrouptable.py `tables_from_pdsfiles` and similar methods, when a path string cannot be resolved as either Pds3File or Pds4File, the program should be allowed to crash rather than silently skipping the file. This fail-fast approach is preferred for invalid path inputs.

Applied to files:

  • viewmaster/pdsiterator.py
  • viewmaster/viewmaster.py
📚 Learning: 2026-01-12T22:07:33.958Z
Learnt from: juzen2003
Repo: SETI/rms-viewmaster PR: 14
File: viewmaster/viewmaster.py:2496-2501
Timestamp: 2026-01-12T22:07:33.958Z
Learning: In viewmaster/viewmaster.py, the if __name__ == '__main__': block that sets debug=True is for development only and should not run in production. Ensure production deployment uses the WSGI server path and that any debug execution is removed or guarded (e.g., do not enable debug mode in the main entry point for production).

Applied to files:

  • viewmaster/viewmaster.py
🧬 Code graph analysis (4)
viewmaster/pdsgroup.py (1)
viewmaster/pdsgrouptable.py (3)
  • iterator (120-126)
  • iterator_for_all (128-134)
  • iterator_for_hidden (136-142)
viewmaster/link.py (1)
viewmaster/viewmaster.py (2)
  • create_logger (81-121)
  • get_or_create_logger (124-136)
viewmaster/pdsiterator.py (2)
viewmaster/pdsgroup.py (2)
  • isdir (135-144)
  • parent_logical_path (122-132)
viewmaster/pdsgrouptable.py (2)
  • filter (390-401)
  • parent_logical_path (82-91)
viewmaster/viewmaster.py (3)
viewmaster/pdsgroup.py (3)
  • PdsGroup (13-598)
  • isdir (135-144)
  • copy (100-119)
viewmaster/link.py (2)
  • create_logger (45-82)
  • get_or_create_logger (84-96)
viewmaster/pdsiterator.py (4)
  • PdsDirIterator (50-231)
  • copy (139-163)
  • copy (303-321)
  • copy (500-510)
🪛 ast-grep (0.40.4)
viewmaster/viewmaster.py

[warning] 2517-2517: Detected Flask app with debug=True. Do not deploy to production with this flag enabled as it will leak sensitive information. Instead, consider using Flask configuration variables or setting 'debug' using system environment variables.
Context: app.run(host='0.0.0.0', port=8080, debug=True)
Note: [CWE-489] Active Debug Code. [REFERENCES]
- https://labs.detectify.com/2015/10/02/how-patreon-got-hacked-publicly-exposed-werkzeug-debugger/

(debug-enabled-python)

🪛 LanguageTool
README.md

[style] ~5-~5: Using many exclamation marks might seem excessive (in this case: 3 exclamation marks for a text that’s 1401 characters long)
Context: ...-----|-------------|----------------| | PyPI version | Build status | [![Code coverage](https://img.shields.io/c...

(EN_EXCESSIVE_EXCLAMATION)

🪛 markdownlint-cli2 (0.18.1)
README.md

3-3: First line in a file should be a top-level heading

(MD041, first-line-heading, first-line-h1)


42-42: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)

🪛 Ruff (0.14.10)
viewmaster/pdsgroup.py

1-1: The file is executable but no shebang is present

(EXE002)


33-33: Missing return type annotation for special method __init__

Add return type annotation: None

(ANN204)


33-33: Boolean default positional argument in function definition

(FBT002)


33-33: Unused method argument: anchor

(ARG002)


74-74: Missing return type annotation for special method __len__

Add return type annotation: int

(ANN204)


83-83: Missing return type annotation for special method __repr__

Add return type annotation: str

(ANN204)


544-544: Unnecessary list comprehension (rewrite using list())

Rewrite using list()

(C416)

viewmaster/viewmaster_config.py

1-1: The file is executable but no shebang is present

(EXE002)


72-72: Probable insecure usage of temporary file or directory: "/var/tmp/memcached.socket"

(S108)


73-73: Probable insecure usage of temporary file or directory: "/var/tmp/memcached.socket"

(S108)

viewmaster/link.py

1-1: The file is executable but no shebang is present

(EXE002)


40-40: from .viewmaster_config import * used; unable to detect undefined names

(F403)


56-56: LOGNAME may be undefined, or defined from star imports

(F405)


60-60: LOG_ROOT_PREFIX_ may be undefined, or defined from star imports

(F405)


67-67: Replace aliased errors with OSError

Replace with builtin OSError

(UP024)


68-68: Logging statement uses f-string

(G004)


142-142: VIEWMASTER_PREFIX_ may be undefined, or defined from star imports

(F405)


146-146: DOCUMENT_ROOT_ may be undefined, or defined from star imports

(F405)

viewmaster/pdsiterator.py

1-1: The file is executable but no shebang is present

(EXE002)


65-65: Missing return type annotation for special method __init__

Add return type annotation: None

(ANN204)


188-188: Missing return type annotation for special method __next__

(ANN204)


259-259: Missing return type annotation for special method __init__

Add return type annotation: None

(ANN204)


323-323: Missing return type annotation for private function _filter_names

(ANN202)


366-366: Missing return type annotation for special method __next__

(ANN204)


426-430: Avoid specifying long messages outside the exception class

(TRY003)


468-468: Missing return type annotation for special method __init__

Add return type annotation: None

(ANN204)


535-535: Missing return type annotation for special method __next__

(ANN204)

viewmaster/viewmaster.py

1-1: The file is executable but no shebang is present

(EXE002)


73-73: from .viewmaster_config import * used; unable to detect undefined names

(F403)


74-74: USE_SHELVES_ONLY may be undefined, or defined from star imports

(F405)


75-75: Boolean positional value in function call

(FBT003)


93-93: LOGNAME may be undefined, or defined from star imports

(F405)


95-95: LOGNAME may be undefined, or defined from star imports

(F405)


98-98: LOG_ROOT_PREFIX_ may be undefined, or defined from star imports

(F405)


108-108: LOG_ROOT_PREFIX_ may be undefined, or defined from star imports

(F405)


232-232: Replace aliased errors with OSError

Replace IOError with builtin OSError

(UP024)


232-232: Avoid specifying long messages outside the exception class

(TRY003)


345-345: Local variable e is assigned to but never used

Remove assignment to unused variable e

(F841)


373-373: PAGE_CACHING may be undefined, or defined from star imports

(F405)


374-374: VIEWMASTER_MEMCACHE_PORT may be undefined, or defined from star imports

(F405)


376-377: Use format specifiers instead of percent format

Replace with format specifiers

(UP031)


376-377: Logging statement uses %

(G002)


377-377: VIEWMASTER_MEMCACHE_PORT may be undefined, or defined from star imports

(F405)


378-378: VIEWMASTER_MEMCACHE_PORT may be undefined, or defined from star imports

(F405)


384-385: Use format specifiers instead of percent format

Replace with format specifiers

(UP031)


384-385: Logging statement uses %

(G002)


385-385: VIEWMASTER_MEMCACHE_PORT may be undefined, or defined from star imports

(F405)


407-407: Boolean default positional argument in function definition

(FBT002)


1353-1353: datetime.datetime.now() called without a tz argument

(DTZ005)


1355-1355: Use format specifiers instead of percent format

Replace with format specifiers

(UP031)


1355-1355: Logging statement uses %

(G002)


2262-2262: Unnecessary pass statement

Remove unnecessary pass

(PIE790)

🔇 Additional comments (4)
viewmaster/pdsiterator.py (1)

417-430: Good error handling in next_cousin.

The for/else pattern correctly raises a descriptive ValueError when neither Pds3File nor Pds4File can create the parent. This pattern should be applied consistently to the copy() methods as noted above.

viewmaster/link.py (1)

99-154: LGTM: Route handler implementation.

The redirect logic correctly distinguishes directories from files using extension detection and handles the 404 case with proper logging. The per-request logger pattern aligns with the broader refactoring goals.

README.md (1)

57-58: Update documentation link after merge.

The TODO comment indicates the readthedocs link needs updating once the branch is merged to main. Consider using a placeholder or the main branch link if the docs are already published.

Would you like me to open an issue to track updating this link post-merge?

⛔ Skipped due to learnings
Learnt from: juzen2003
Repo: SETI/rms-viewmaster PR: 14
File: README.md:16-18
Timestamp: 2026-01-12T21:03:16.187Z
Learning: In the rms-viewmaster repository, the README.md format should match the README format used in other SETI repositories (such as rms-starcat) for consistency, even if it results in MD031 linting violations (missing blank lines around fenced code blocks).
Learnt from: juzen2003
Repo: SETI/rms-viewmaster PR: 14
File: docs/Makefile:11-20
Timestamp: 2025-12-12T23:26:32.767Z
Learning: In the rms-viewmaster repository, the `docs/Makefile` is auto-generated by Sphinx and should not be modified, even if linters flag missing standard phony targets.
viewmaster/viewmaster.py (1)

2509-2519: No action required—this is development-only code.

The __main__ block is intentionally preserved for local development and is not executed in production, which uses a WSGI server setup (see wsgi_init.py). The debug=True and 0.0.0.0 binding pose no production risk in this context.

Comment on lines +32 to +37
4. Create and set ownership for `/Library/WebServer` to avoid permission issues:

```bash
sudo mkdir /Library/WebServer
sudo chown -R user /Library/WebServer # Replace "user" with your own username
```
Copy link

@coderabbitai coderabbitai bot Jan 12, 2026

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Avoid requiring sudo for local development setup.

The instructions require sudo to create /Library/WebServer, which was flagged in PR discussions as indicating a configuration/permission problem. Consider either:

  1. Using a user-writable directory (e.g., ~/.viewmaster/ or a directory within the repo)
  2. Making this path configurable via an environment variable

This would allow developers to run viewmaster without elevated privileges.

🤖 Prompt for AI Agents
In @README.md around lines 32 - 37, The README currently instructs creating
/Library/WebServer with sudo; change this to avoid requiring elevated privileges
by updating the docs and the app to use a user-writable default and an
environment variable override: replace the hardcoded /Library/WebServer
recommendation with a user-local default (e.g., ~/.viewmaster or a repo-local
./viewmaster_data) and document an env var like VIEWMASTER_WEBROOT (or similar)
that the application reads to set the webroot; update README to show creating a
user-writable directory (no sudo) and how to set VIEWMASTER_WEBROOT, and change
any code that assumes /Library/WebServer to read the new env var and fall back
to the user-local default.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Leave as-is, the default WEBSITE_ROOT_, DOCUMENT_ROOT_, and LOG_ROOT_PREFIX_ are all under /Library/WebServer. We don't plan to change it at this point.

Copy link

Choose a reason for hiding this comment

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

@juzen2003, understood! I've noted your preference to keep the /Library/WebServer default configuration.


🧠 Learnings used
Learnt from: juzen2003
Repo: SETI/rms-viewmaster PR: 14
File: viewmaster/viewmaster.py:2496-2501
Timestamp: 2026-01-12T22:07:39.297Z
Learning: In the rms-viewmaster repository, the `if __name__ == "__main__":` block in `viewmaster/viewmaster.py` with `debug=True` is intentionally kept for development purposes only. This code path is not executed in production, which uses a different deployment method (e.g., WSGI server).

Learnt from: juzen2003
Repo: SETI/rms-viewmaster PR: 14
File: README.md:16-18
Timestamp: 2026-01-12T21:03:22.730Z
Learning: In the rms-viewmaster repository, the README.md format should match the README format used in other SETI repositories (such as rms-starcat) for consistency, even if it results in MD031 linting violations (missing blank lines around fenced code blocks).

Copy link
Collaborator

Choose a reason for hiding this comment

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

This is not quite true. These are not the directories to use under Linux. We use /var/www. But I expect this documentation will end up being mostly wrong anyway by the time we finish.

Copy link

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Comment on lines 45 to 96
def create_logger():
"""Create and configure a logger for the Link service.
# DEBUG_LOG_FILE = LOG_ROOT_PREFIX_ + 'link_debug.log'
# debug_logfile = os.path.abspath(DEBUG_LOG_FILE)
# debug_handler = pdslogger.file_handler(debug_logfile, level=logging.DEBUG,
# rotation='midnight')
# LOGGER.add_handler(debug_handler)
Sets up a PdsLogger instance with INFO level logging to a file with
midnight rotation. The logger name is derived from the Viewmaster logger
name by replacing 'viewmaster' with 'link'.
################################################################################
Returns:
pdslogger.PdsLogger: Configured logger instance.
"""

link_logger_name = LOGNAME.replace('viewmaster', 'link')
logger = pdslogger.PdsLogger(link_logger_name, limits={'info': -1, 'normal': -1},
pid=True)

LOG_FILE = LOG_ROOT_PREFIX_ + 'link.log'
info_logfile = os.path.abspath(LOG_FILE)

try:
info_handler = pdslogger.file_handler(info_logfile, level=logging.INFO,
rotation='midnight')
logger.add_handler(info_handler)
except (OSError, IOError) as e:
logger.warning(f'Could not open log file {info_logfile}: {e}')

# DEBUG_LOG_FILE = LOG_ROOT_PREFIX_ + 'link_debug.log'
# debug_logfile = os.path.abspath(DEBUG_LOG_FILE)
# debug_handler = pdslogger.file_handler(debug_logfile, level=logging.DEBUG,
# rotation='midnight')
# logger.add_handler(debug_handler)

################################################################################

logger.blankline()
logger.blankline()
logger.info('Starting Link', info_logfile)

return logger

def get_or_create_logger():
"""Get the cached logger instance, creating it if necessary.
This ensures the logger and its handlers are only created once, preventing
duplicate handlers from being added on subsequent requests.
Returns:
pdslogger.PdsLogger: The cached logger instance.
"""
global LOGGER
if LOGGER is None:
LOGGER = create_logger()
return LOGGER
Copy link

@coderabbitai coderabbitai bot Jan 12, 2026

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider extracting shared logger utilities to reduce duplication.

The create_logger() and get_or_create_logger() functions here closely mirror those in viewmaster/viewmaster.py (lines 80-135). Consider extracting common logger setup logic to a shared module (e.g., viewmaster/logging_utils.py) to reduce duplication and ensure consistent configuration.

🧰 Tools
🪛 Ruff (0.14.10)

56-56: LOGNAME may be undefined, or defined from star imports

(F405)


60-60: LOG_ROOT_PREFIX_ may be undefined, or defined from star imports

(F405)


67-67: Replace aliased errors with OSError

Replace with builtin OSError

(UP024)


68-68: Logging statement uses f-string

(G004)

🤖 Prompt for AI Agents
In @viewmaster/link.py around lines 45 - 96, Extract the duplicated logger setup
into a shared helper (e.g., a new function in viewmaster/logging_utils.py) that
encapsulates creating a pdslogger.PdsLogger, adding the file handler with
rotation, handling OSError/IOError, and performing the two blankline/info
startup call; then replace Link's create_logger() to call that helper (passing
the LOGNAME replacement 'viewmaster'->'link', LOG_ROOT_PREFIX_+'link.log',
limits and pid flags) and keep get_or_create_logger() as a thin cached wrapper
that delegates to the shared helper; ensure unique symbols to update are
create_logger, get_or_create_logger in viewmaster/link.py and the new exported
helper name (e.g., setup_pds_logger) in the shared module.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Leave as-is

Copy link

Choose a reason for hiding this comment

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

@juzen2003, understood! I'll leave the logger setup as-is in both files.


🧠 Learnings used
Learnt from: juzen2003
Repo: SETI/rms-viewmaster PR: 14
File: README.md:16-18
Timestamp: 2026-01-12T21:03:22.730Z
Learning: In the rms-viewmaster repository, the README.md format should match the README format used in other SETI repositories (such as rms-starcat) for consistency, even if it results in MD031 linting violations (missing blank lines around fenced code blocks).

Comment on lines +38 to +39
# Test for a command line run, python viewmaster/viewmaster.py
VIEWMASTER_TESTING = sys.argv[0].endswith('viewmaster.py')
Copy link

@coderabbitai coderabbitai bot Jan 12, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Search for VIEWMASTER_TESTING usage
rg "VIEWMASTER_TESTING" --type py -B 2 -A 2

Repository: SETI/rms-viewmaster

Length of output: 1293


🏁 Script executed:

# Check for any scripts ending in viewmaster.py
fd -e py | grep viewmaster

Repository: SETI/rms-viewmaster

Length of output: 238


🏁 Script executed:

# Examine the viewmaster_config.py file to understand context
cat -n viewmaster/viewmaster_config.py | head -100

Repository: SETI/rms-viewmaster

Length of output: 4081


🏁 Script executed:

# Check the main viewmaster.py script to understand deployment
cat -n viewmaster/viewmaster.py | head -50

Repository: SETI/rms-viewmaster

Length of output: 1715


Testing mode detection could unintentionally trigger for any script ending with viewmaster.py.

Using sys.argv[0].endswith('viewmaster.py') could match unintended scripts like test_viewmaster.py or my_viewmaster.py, though this is unlikely given the current codebase and production deployment via WSGI. If strengthening this check, consider an environment variable like VIEWMASTER_TESTING=1 for explicit control, which is simpler than path pattern matching.

🤖 Prompt for AI Agents
In @viewmaster/viewmaster_config.py around lines 38 - 39, Replace the fragile
filename check for VIEWMASTER_TESTING with an explicit
environment-variable-based toggle: check os.environ.get('VIEWMASTER_TESTING')
(e.g. equals '1' or a truthy value) to determine testing mode, falling back to
the existing sys.argv[0].endswith('viewmaster.py') only if the env var is not
set; update the VIEWMASTER_TESTING assignment and add an import for os if
missing so the new env-var check is used by the module.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Leave as-is

Copy link

Choose a reason for hiding this comment

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

@juzen2003, understood. I'll keep the current implementation as-is.


🧠 Learnings used
Learnt from: juzen2003
Repo: SETI/rms-viewmaster PR: 14
File: viewmaster/viewmaster.py:2496-2501
Timestamp: 2026-01-12T22:07:39.297Z
Learning: In the rms-viewmaster repository, the `if __name__ == "__main__":` block in `viewmaster/viewmaster.py` with `debug=True` is intentionally kept for development purposes only. This code path is not executed in production, which uses a different deployment method (e.g., WSGI server).

Learnt from: juzen2003
Repo: SETI/rms-viewmaster PR: 14
File: README.md:16-18
Timestamp: 2026-01-12T21:03:22.730Z
Learning: In the rms-viewmaster repository, the README.md format should match the README format used in other SETI repositories (such as rms-starcat) for consistency, even if it results in MD031 linting violations (missing blank lines around fenced code blocks).

Updated the import statements in link.py to use a more structured format, enhancing readability and maintainability of the code. This change groups related configuration variables together, making it easier to understand dependencies at a glance.
Updated the exception handling in create_logger to remove the IOError catch, simplifying the error logging process. This change enhances the clarity of log messages when failing to open the log file.
Updated the member retrieval method to use the built-in list constructor instead of a list comprehension. This change enhances code clarity and maintains consistency in the way members are returned.
Refactored the create_logger function to check for existing handlers before adding new ones, preventing duplicates. Added exception handling for file handler creation to log warnings when log files cannot be opened, improving the robustness and clarity of the logging process.
…larity

Updated the Memcache connection logic to use a local variable for the port, enhancing readability and simplifying error handling. This change ensures that the fallback to DictionaryCache is clearer and more robust, improving the overall caching mechanism.
…nality

Updated the return_holdings_local function to utilize send_from_directory for serving files, improving the handling of MIME types and simplifying the code structure. This change enhances the clarity of file serving logic and ensures better adherence to Flask's best practices.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

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

⚠️ Outside diff range comments (2)
viewmaster/pdsgroup.py (1)

296-403: Duplicate sort() method definition.

The sort() method is defined twice with identical signatures (lines 296-348 and lines 350-403). In Python, the second definition completely shadows the first, making the first implementation dead code. The implementations appear nearly identical—remove one of them.

🐛 Proposed fix: remove duplicate definition
-    def sort(self, labels_after=None, dirs_first=None, dirs_last=None,
-                   info_first=None):
-        """Sort the rows of this group.
-
-        Parameters:
-            labels_after (bool|None): Place labels after their targets.
-            dirs_first (bool|None): Sort directories before files.
-            dirs_last (bool|None): Sort directories after files.
-            info_first (bool|None): Prioritize info files.
-
-        Returns:
-            None
-        """
-
-        basename_dict = {pdsf.basename:pdsf for pdsf in self.rows}
-
-        if self.parent_pdsf:
-            sorted = self.parent_pdsf.sort_basenames(list(basename_dict.keys()),
-                                                labels_after=labels_after,
-                                                dirs_first=dirs_first,
-                                                dirs_last=dirs_last,
-                                                info_first=info_first)
-
-            # If there are multiple labels in the group, make sure each label
-            # is adjacent to any data files of the same name. This can comes up
-            # with Juno JIRAM, at least.
-            labels = [basename for basename in sorted
-                      if self.parent_pdsf.basename_is_label(basename)]
-            if len(labels) > 1:
-              for label in labels:
-                l = sorted.index(label)                     # index of label
-                pattern = os.path.splitext(label)[0] + '.'
-                tlist = [t for t in range(len(sorted))      # indices of targets
-                         if sorted[t].startswith(pattern)]
-                tlist.remove(l)
-
-                # If the label is not adjacent, but could be...
-                if tlist and (l+1) not in tlist and (l-1) not in tlist:
-                    # Locate the nearest target with a matching basename
-                    diffs = [(abs(t-l),t) for t in tlist]
-                    tbest = min(diffs)[1]
-
-                    # Relocate the label to an adjacent location
-                    if l < tbest:
-                        sorted.insert(tbest - 1, sorted.pop(l))
-                    else:
-                        sorted.insert(tbest + 1, sorted.pop(l))
-
-        else:
-            sorted = list(basename_dict.keys())     # for merged directories
-            sorted.sort()
-
-        self.rows = [basename_dict[key] for key in sorted]
-
     def sort(self, labels_after=None, dirs_first=None, dirs_last=None,
                    info_first=None):
         """Sort member files by parent-defined rules with tweaks for labels.
viewmaster/viewmaster.py (1)

1783-1818: Bug: get_query_params_from_dict returns cleaned version of wrong variable.

Line 1818 returns clean_query_params(params) but should return clean_query_params(new_params). The function builds new_params with defaults applied, but then cleans and returns the original params dict, ignoring all the work done.

🐛 Proposed fix
     # Fill in default values for missing keys
     for key in defaults:
         if key not in new_params:
             new_params[key] = defaults[key]

-    return clean_query_params(params)
+    return clean_query_params(new_params)
🤖 Fix all issues with AI agents
In `@viewmaster/pdsgroup.py`:
- Line 33: The __init__ of PdsGroup currently accepts an unused parameter
anchor; either remove the parameter or assign it to the instance. To implement
the intended behavior, set self.anchor = anchor (or a default if None) in
PdsGroup.__init__, and ensure any existing logic that derives anchor from the
first appended PdsFile (e.g., code in append/appendPdsFile or methods
referencing self.anchor) respects an already-set self.anchor (i.e., do not
overwrite it if anchor was provided). Update any docstring/comments to reflect
that anchor can be provided by the constructor.

In `@viewmaster/viewmaster.py`:
- Around line 2274-2280: Remove the redundant `pass` inside the except block
that follows the `LOGGER.warning('Fancy index unavailable; abort(404)')` call;
locate the try/except surrounding the redirect (the block that logs "Fancy index
unavailable; abort(404)") and simply delete the `pass` line so the except block
contains only the warning call and control flow continues to the subsequent
`LOGGER.warning('ABORT 404')`.
- Around line 80-86: Replace the wildcard import from viewmaster_config with
explicit named imports: import USE_SHELVES_ONLY and every other config symbol
referenced in this module (e.g., LOGNAME, LOG_ROOT_PREFIX_, VIEWMASTER_PREFIX_,
DOCUMENT_ROOT_, WEBSITE_HTTP_HOME, LOCALHOST_, VIEWMASTER_MEMCACHE_PORT,
PDSFILE_MEMCACHE_PORT, PAGE_CACHING, EXTRA_LOCAL_IP_ADDRESS_A_B_C, etc.) so
static analysis no longer reports F403/F405; keep the
Pds3File.use_shelves_only(True) call but ensure Pds3File is imported or
available and update the top-of-file imports to list each required symbol
explicitly rather than using from .viewmaster_config import *.
- Around line 235-249: The function get_holdings_paths currently raises IOError;
replace that with OSError for consistency with the rest of the file (e.g., other
OSError uses) and update the docstring "Raises" section to list OSError instead
of IOError; specifically change the raise statement in get_holdings_paths to
raise OSError("'PDS3_HOLDINGS_DIR' environment variable not set") and adjust the
Returns/Raises docstring accordingly.
- Around line 359-369: The except block captures an exception as variable `e`
but never uses it; either remove the unused `e` (change to `except Exception:`)
or include the exception in the log (e.g., `logger.exception('Failed to get or
validate holdings path: %s', e)` or rely on `logger.exception`'s traceback by
passing `exc_info=True`), updating the except clause around
`get_holdings_paths`/`validate_holdings_paths` accordingly.
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8c13fb8 and 3f8d0ed.

📒 Files selected for processing (3)
  • viewmaster/link.py
  • viewmaster/pdsgroup.py
  • viewmaster/viewmaster.py
🧰 Additional context used
🧠 Learnings (6)
📓 Common learnings
Learnt from: juzen2003
Repo: SETI/rms-viewmaster PR: 14
File: README.md:16-18
Timestamp: 2026-01-12T21:03:22.730Z
Learning: In the rms-viewmaster repository, the README.md format should match the README format used in other SETI repositories (such as rms-starcat) for consistency, even if it results in MD031 linting violations (missing blank lines around fenced code blocks).
📚 Learning: 2026-01-12T21:03:22.730Z
Learnt from: juzen2003
Repo: SETI/rms-viewmaster PR: 14
File: README.md:16-18
Timestamp: 2026-01-12T21:03:22.730Z
Learning: In the rms-viewmaster repository, the README.md format should match the README format used in other SETI repositories (such as rms-starcat) for consistency, even if it results in MD031 linting violations (missing blank lines around fenced code blocks).

Applied to files:

  • viewmaster/link.py
  • viewmaster/viewmaster.py
📚 Learning: 2025-12-18T21:42:57.270Z
Learnt from: juzen2003
Repo: SETI/rms-viewmaster PR: 14
File: viewmaster/pdsgrouptable.py:21-22
Timestamp: 2025-12-18T21:42:57.270Z
Learning: In viewmaster, use `pdsfile.Pds3File` explicitly in code implementation instead of the generic `pdsfile.PdsFile` base class in pdsgrouptable.py and pdsiterator.py. Docstrings should reference `Pds3File`, with `Pds4File` to be added later when PDS4 is fully supported.

Applied to files:

  • viewmaster/pdsgroup.py
  • viewmaster/viewmaster.py
📚 Learning: 2026-01-12T22:07:33.958Z
Learnt from: juzen2003
Repo: SETI/rms-viewmaster PR: 14
File: viewmaster/viewmaster.py:2496-2501
Timestamp: 2026-01-12T22:07:33.958Z
Learning: In viewmaster/viewmaster.py, the if __name__ == '__main__': block that sets debug=True is for development only and should not run in production. Ensure production deployment uses the WSGI server path and that any debug execution is removed or guarded (e.g., do not enable debug mode in the main entry point for production).

Applied to files:

  • viewmaster/viewmaster.py
📚 Learning: 2025-12-12T23:26:32.767Z
Learnt from: juzen2003
Repo: SETI/rms-viewmaster PR: 14
File: docs/Makefile:11-20
Timestamp: 2025-12-12T23:26:32.767Z
Learning: In the rms-viewmaster repository, the `docs/Makefile` is auto-generated by Sphinx and should not be modified, even if linters flag missing standard phony targets.

Applied to files:

  • viewmaster/viewmaster.py
📚 Learning: 2026-01-12T21:13:24.268Z
Learnt from: juzen2003
Repo: SETI/rms-viewmaster PR: 14
File: viewmaster/pdsgrouptable.py:460-467
Timestamp: 2026-01-12T21:13:24.268Z
Learning: In viewmaster's pdsgrouptable.py `tables_from_pdsfiles` and similar methods, when a path string cannot be resolved as either Pds3File or Pds4File, the program should be allowed to crash rather than silently skipping the file. This fail-fast approach is preferred for invalid path inputs.

Applied to files:

  • viewmaster/viewmaster.py
🧬 Code graph analysis (2)
viewmaster/link.py (1)
viewmaster/viewmaster.py (2)
  • create_logger (88-138)
  • get_or_create_logger (141-153)
viewmaster/pdsgroup.py (1)
viewmaster/pdsgrouptable.py (3)
  • iterator (120-126)
  • iterator_for_all (128-134)
  • iterator_for_hidden (136-142)
🪛 ast-grep (0.40.5)
viewmaster/viewmaster.py

[warning] 2532-2532: Detected Flask app with debug=True. Do not deploy to production with this flag enabled as it will leak sensitive information. Instead, consider using Flask configuration variables or setting 'debug' using system environment variables.
Context: app.run(host='0.0.0.0', port=8080, debug=True)
Note: [CWE-489] Active Debug Code. [REFERENCES]
- https://labs.detectify.com/2015/10/02/how-patreon-got-hacked-publicly-exposed-werkzeug-debugger/

(debug-enabled-python)

🪛 Ruff (0.14.11)
viewmaster/link.py

1-1: The file is executable but no shebang is present

(EXE002)


74-74: Logging statement uses f-string

(G004)

viewmaster/pdsgroup.py

1-1: The file is executable but no shebang is present

(EXE002)


33-33: Missing return type annotation for special method __init__

Add return type annotation: None

(ANN204)


33-33: Boolean default positional argument in function definition

(FBT002)


33-33: Unused method argument: anchor

(ARG002)


74-74: Missing return type annotation for special method __len__

Add return type annotation: int

(ANN204)


83-83: Missing return type annotation for special method __repr__

Add return type annotation: str

(ANN204)

viewmaster/viewmaster.py

1-1: The file is executable but no shebang is present

(EXE002)


80-80: from .viewmaster_config import * used; unable to detect undefined names

(F403)


81-81: USE_SHELVES_ONLY may be undefined, or defined from star imports

(F405)


82-82: Boolean positional value in function call

(FBT003)


100-100: LOGNAME may be undefined, or defined from star imports

(F405)


102-102: LOGNAME may be undefined, or defined from star imports

(F405)


105-105: LOG_ROOT_PREFIX_ may be undefined, or defined from star imports

(F405)


119-119: Logging statement uses f-string

(G004)


122-122: LOG_ROOT_PREFIX_ may be undefined, or defined from star imports

(F405)


130-130: Logging statement uses f-string

(G004)


249-249: Replace aliased errors with OSError

Replace IOError with builtin OSError

(UP024)


249-249: Avoid specifying long messages outside the exception class

(TRY003)


362-362: Local variable e is assigned to but never used

Remove assignment to unused variable e

(F841)


390-390: PAGE_CACHING may be undefined, or defined from star imports

(F405)


391-391: VIEWMASTER_MEMCACHE_PORT may be undefined, or defined from star imports

(F405)


394-395: Use format specifiers instead of percent format

Replace with format specifiers

(UP031)


394-395: Logging statement uses %

(G002)


402-403: Use format specifiers instead of percent format

Replace with format specifiers

(UP031)


402-403: Logging statement uses %

(G002)


425-425: Boolean default positional argument in function definition

(FBT002)


1371-1371: datetime.datetime.now() called without a tz argument

(DTZ005)


1373-1373: Use format specifiers instead of percent format

Replace with format specifiers

(UP031)


1373-1373: Logging statement uses %

(G002)


2277-2277: Unnecessary pass statement

Remove unnecessary pass

(PIE790)

🔇 Additional comments (22)
viewmaster/link.py (4)

1-14: LGTM!

The module docstring provides a clear overview of the Link redirect service functionality, including directory detection logic and redirect behavior.


40-46: LGTM!

The explicit imports from viewmaster_config are well-structured, replacing the previous star import. This improves code clarity and satisfies static analysis requirements.


48-102: LGTM!

The logger creation and caching pattern is well-implemented:

  • create_logger() properly handles file handler errors with a warning instead of crashing
  • get_or_create_logger() provides proper singleton caching to avoid duplicate handlers
  • The pattern mirrors viewmaster.py appropriately for this module's needs

105-160: LGTM!

The route handler correctly:

  • Uses get_or_create_logger() to avoid per-request logger creation
  • Implements directory detection via extension/digit heuristics
  • Uses glob patterns to locate files across holdings symlinks
  • Logs actions appropriately for debugging
viewmaster/pdsgroup.py (5)

1-31: LGTM!

The module docstring and class documentation are comprehensive, clearly explaining the purpose of PdsGroup and its attributes.


33-72: LGTM!

The __init__ method correctly uses None as defaults for mutable arguments (pdsfiles and hidden), avoiding the shared-state bug that was previously flagged. The initialization logic properly handles all input types.


74-119: LGTM!

The __len__, __repr__, and copy() implementations are correct and well-documented.


405-552: LGTM!

The append/remove/hide/unhide methods and iterator helpers are well-implemented with proper validation, duplicate detection, and cache invalidation.


558-598: LGTM!

The group_children static method provides a clean interface for grouping a directory's children by anchor.

viewmaster/viewmaster.py (13)

1-18: LGTM!

The module docstring provides a clear overview of the Viewmaster Flask application, including environment variable and configuration requirements.


88-153: LGTM!

The logger creation and caching implementation is well-structured:

  • create_logger() guards against duplicate handlers with has_handlers check
  • Error handling wraps file handler creation to prevent crashes
  • get_or_create_logger() provides proper singleton pattern
  • Both INFO and DEBUG log files are handled consistently

372-417: LGTM!

The get_page_cache() function properly:

  • Uses a local memcache_port variable to avoid the UnboundLocalError
  • Falls back gracefully to DictionaryCache on Memcache failure
  • Logs the cache backend being used

425-447: LGTM!

The initialize_caches() function correctly initializes holdings and page caches, with optional reset capability.


559-671: LGTM!

The get_prev_next_navigation() function is well-implemented with proper iterator handling, navigation constraints, and edge case handling for virtual directories and index rows.


981-1057: LGTM!

The get_directory_page() function properly assembles page data including groups, associations, documents, navigation, and info content.


1061-1275: LGTM!

The directory_page_html() function handles filtering, pagination, row ranges, and link generation correctly, with proper redirect handling when parameters need adjustment.


1281-1447: LGTM!

The get_product_page_info() function handles complex product page assembly including siblings, info files, navigation, and index row special cases.


1451-1659: LGTM!

The product_page_html() function properly renders product views with filtering, multipage support, and special handling for index rows.


2124-2139: LGTM!

The return_holdings_local() function correctly uses send_from_directory() which provides path traversal protection, addressing the previously flagged security concern.


2157-2284: LGTM!

The main viewmaster() route handler properly:

  • Uses cached pages when available
  • Handles redirects for path normalization and parameter adjustment
  • Logs requests with timing information
  • Handles errors gracefully with fallback to fancy index or 404 page

2362-2423: LGTM!

The fill_page_cache() function properly walks the holdings structure to warm the cache. The logger parameter is correctly threaded through.


2524-2533: LGTM!

The __main__ block correctly initializes the logger, holdings paths, and page cache before starting the Flask development server. Per previous discussion, debug=True is acceptable here since this block is only executed during development.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Modified the anchor attribute in the PdsGroup class to accept an argument during initialization, enhancing the clarity of its purpose and ensuring it is set correctly upon object creation. This change improves the overall readability and maintainability of the code.
Updated the import statements to use a structured format, grouping related configuration variables together. This change enhances readability and maintainability of the code, making dependencies clearer at a glance.
Updated the exception handling in multiple functions to raise OSError instead of IOError when the PDS3_HOLDINGS_DIR environment variable is not set or when no valid holdings paths remain. This change improves consistency in error handling and aligns with Python's standard exceptions for operating system-related errors.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (4)
viewmaster/pdsgroup.py (2)

296-403: Critical: Duplicate sort method definition.

The sort method is defined twice (lines 296-348 and lines 350-403). The second definition completely shadows the first, making the first implementation dead code. Both implementations appear identical, so this is likely a copy-paste error.

🐛 Proposed fix: Remove the duplicate definition
-    def sort(self, labels_after=None, dirs_first=None, dirs_last=None,
-                   info_first=None):
-        """Sort the rows of this group.
-
-        Parameters:
-            labels_after (bool|None): Place labels after their targets.
-            dirs_first (bool|None): Sort directories before files.
-            dirs_last (bool|None): Sort directories after files.
-            info_first (bool|None): Prioritize info files.
-
-        Returns:
-            None
-        """
-
-        basename_dict = {pdsf.basename:pdsf for pdsf in self.rows}
-
-        if self.parent_pdsf:
-            sorted = self.parent_pdsf.sort_basenames(list(basename_dict.keys()),
-                                                labels_after=labels_after,
-                                                dirs_first=dirs_first,
-                                                dirs_last=dirs_last,
-                                                info_first=info_first)
-
-            # If there are multiple labels in the group, make sure each label
-            # is adjacent to any data files of the same name. This can comes up
-            # with Juno JIRAM, at least.
-            labels = [basename for basename in sorted
-                      if self.parent_pdsf.basename_is_label(basename)]
-            if len(labels) > 1:
-              for label in labels:
-                l = sorted.index(label)                     # index of label
-                pattern = os.path.splitext(label)[0] + '.'
-                tlist = [t for t in range(len(sorted))      # indices of targets
-                         if sorted[t].startswith(pattern)]
-                tlist.remove(l)
-
-                # If the label is not adjacent, but could be...
-                if tlist and (l+1) not in tlist and (l-1) not in tlist:
-                    # Locate the nearest target with a matching basename
-                    diffs = [(abs(t-l),t) for t in tlist]
-                    tbest = min(diffs)[1]
-
-                    # Relocate the label to an adjacent location
-                    if l < tbest:
-                        sorted.insert(tbest - 1, sorted.pop(l))
-                    else:
-                        sorted.insert(tbest + 1, sorted.pop(l))
-
-        else:
-            sorted = list(basename_dict.keys())     # for merged directories
-            sorted.sort()
-
-        self.rows = [basename_dict[key] for key in sorted]
-
     def sort(self, labels_after=None, dirs_first=None, dirs_last=None,
                    info_first=None):
         """Sort member files by parent-defined rules with tweaks for labels.

379-384: Inconsistent indentation in nested loop.

The for loop body at line 379 uses 2-space indentation while the rest of the codebase uses 4-space indentation.

🔧 Proposed fix
             if len(labels) > 1:
-              for label in labels:
-                l = sorted.index(label)                     # index of label
-                pattern = os.path.splitext(label)[0] + '.'
-                tlist = [t for t in range(len(sorted))      # indices of targets
-                         if sorted[t].startswith(pattern)]
-                tlist.remove(l)
+                for label in labels:
+                    l = sorted.index(label)                     # index of label
+                    pattern = os.path.splitext(label)[0] + '.'
+                    tlist = [t for t in range(len(sorted))      # indices of targets
+                             if sorted[t].startswith(pattern)]
+                    tlist.remove(l)
viewmaster/viewmaster.py (2)

2211-2213: Redundant exception object in logger.exception() call.

logger.exception(e) passes the exception object redundantly since exception() automatically captures the current exception info. This is similar to the pattern that was fixed elsewhere in this file.

🔧 Proposed fix
         except Exception as e:
-            LOGGER.exception(e)
+            LOGGER.exception('Cache retrieval failed')
             pass

1820-1832: Bug: get_query_params_from_dict returns cleaned original params instead of new_params.

Line 1832 returns clean_query_params(params) but should return clean_query_params(new_params). The function modifies new_params but then ignores it, returning the cleaned original params instead.

🐛 Proposed fix
     # Fill in default values for missing keys
     for key in defaults:
         if key not in new_params:
             new_params[key] = defaults[key]
 
-    return clean_query_params(params)
+    return clean_query_params(new_params)
🤖 Fix all issues with AI agents
In `@viewmaster/viewmaster.py`:
- Around line 1383-1388: The timestamp uses naive datetime (start_time =
datetime.datetime.now()) which can cause timezone issues; replace calls to
datetime.datetime.now() with timezone-aware calls (e.g.,
datetime.datetime.now(datetime.timezone.utc)) in this block around start_time
and elapsed calculation and similarly update other occurrences in viewmaster()
and directory_page_html(); ensure get_prev_next_navigation timing and the
logger.warning call still reference the new start_time/elapsed variables and
that imported datetime.timezone is available.
- Around line 2434-2437: The local assignment to PAGE_CACHE shadows the
module-level variable and has no effect; remove the redundant "PAGE_CACHE =
get_page_cache(logger)" line and simply call the cached instance if present
(i.e., if PAGE_CACHE: PAGE_CACHE.flush()), or if the intent truly is to rebind
the module-level PAGE_CACHE, add "global PAGE_CACHE" at the top of the function
and then assign; reference PAGE_CACHE and get_page_cache to locate the code to
change.
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3f8d0ed and 7a07718.

📒 Files selected for processing (2)
  • viewmaster/pdsgroup.py
  • viewmaster/viewmaster.py
🧰 Additional context used
🧠 Learnings (6)
📓 Common learnings
Learnt from: juzen2003
Repo: SETI/rms-viewmaster PR: 14
File: README.md:16-18
Timestamp: 2026-01-12T21:03:22.730Z
Learning: In the rms-viewmaster repository, the README.md format should match the README format used in other SETI repositories (such as rms-starcat) for consistency, even if it results in MD031 linting violations (missing blank lines around fenced code blocks).
📚 Learning: 2025-12-18T21:42:57.270Z
Learnt from: juzen2003
Repo: SETI/rms-viewmaster PR: 14
File: viewmaster/pdsgrouptable.py:21-22
Timestamp: 2025-12-18T21:42:57.270Z
Learning: In viewmaster, use `pdsfile.Pds3File` explicitly in code implementation instead of the generic `pdsfile.PdsFile` base class in pdsgrouptable.py and pdsiterator.py. Docstrings should reference `Pds3File`, with `Pds4File` to be added later when PDS4 is fully supported.

Applied to files:

  • viewmaster/pdsgroup.py
  • viewmaster/viewmaster.py
📚 Learning: 2026-01-12T22:07:33.958Z
Learnt from: juzen2003
Repo: SETI/rms-viewmaster PR: 14
File: viewmaster/viewmaster.py:2496-2501
Timestamp: 2026-01-12T22:07:33.958Z
Learning: In viewmaster/viewmaster.py, the if __name__ == '__main__': block that sets debug=True is for development only and should not run in production. Ensure production deployment uses the WSGI server path and that any debug execution is removed or guarded (e.g., do not enable debug mode in the main entry point for production).

Applied to files:

  • viewmaster/viewmaster.py
📚 Learning: 2026-01-12T21:03:22.730Z
Learnt from: juzen2003
Repo: SETI/rms-viewmaster PR: 14
File: README.md:16-18
Timestamp: 2026-01-12T21:03:22.730Z
Learning: In the rms-viewmaster repository, the README.md format should match the README format used in other SETI repositories (such as rms-starcat) for consistency, even if it results in MD031 linting violations (missing blank lines around fenced code blocks).

Applied to files:

  • viewmaster/viewmaster.py
📚 Learning: 2025-12-12T23:26:32.767Z
Learnt from: juzen2003
Repo: SETI/rms-viewmaster PR: 14
File: docs/Makefile:11-20
Timestamp: 2025-12-12T23:26:32.767Z
Learning: In the rms-viewmaster repository, the `docs/Makefile` is auto-generated by Sphinx and should not be modified, even if linters flag missing standard phony targets.

Applied to files:

  • viewmaster/viewmaster.py
📚 Learning: 2026-01-12T21:13:24.268Z
Learnt from: juzen2003
Repo: SETI/rms-viewmaster PR: 14
File: viewmaster/pdsgrouptable.py:460-467
Timestamp: 2026-01-12T21:13:24.268Z
Learning: In viewmaster's pdsgrouptable.py `tables_from_pdsfiles` and similar methods, when a path string cannot be resolved as either Pds3File or Pds4File, the program should be allowed to crash rather than silently skipping the file. This fail-fast approach is preferred for invalid path inputs.

Applied to files:

  • viewmaster/viewmaster.py
🧬 Code graph analysis (1)
viewmaster/pdsgroup.py (1)
viewmaster/pdsgrouptable.py (3)
  • iterator (120-126)
  • iterator_for_all (128-134)
  • iterator_for_hidden (136-142)
🪛 ast-grep (0.40.5)
viewmaster/viewmaster.py

[warning] 2546-2546: Detected Flask app with debug=True. Do not deploy to production with this flag enabled as it will leak sensitive information. Instead, consider using Flask configuration variables or setting 'debug' using system environment variables.
Context: app.run(host='0.0.0.0', port=8080, debug=True)
Note: [CWE-489] Active Debug Code. [REFERENCES]
- https://labs.detectify.com/2015/10/02/how-patreon-got-hacked-publicly-exposed-werkzeug-debugger/

(debug-enabled-python)

🪛 Ruff (0.14.11)
viewmaster/pdsgroup.py

1-1: The file is executable but no shebang is present

(EXE002)


33-33: Missing return type annotation for special method __init__

Add return type annotation: None

(ANN204)


33-33: Boolean default positional argument in function definition

(FBT002)


74-74: Missing return type annotation for special method __len__

Add return type annotation: int

(ANN204)


83-83: Missing return type annotation for special method __repr__

Add return type annotation: str

(ANN204)

viewmaster/viewmaster.py

1-1: The file is executable but no shebang is present

(EXE002)


96-96: Boolean positional value in function call

(FBT003)


133-133: Logging statement uses f-string

(G004)


144-144: Logging statement uses f-string

(G004)


263-263: Avoid specifying long messages outside the exception class

(TRY003)


346-346: Avoid specifying long messages outside the exception class

(TRY003)


408-409: Use format specifiers instead of percent format

Replace with format specifiers

(UP031)


408-409: Logging statement uses %

(G002)


416-417: Use format specifiers instead of percent format

Replace with format specifiers

(UP031)


416-417: Logging statement uses %

(G002)


439-439: Boolean default positional argument in function definition

(FBT002)


1385-1385: datetime.datetime.now() called without a tz argument

(DTZ005)


1387-1387: Use format specifiers instead of percent format

Replace with format specifiers

(UP031)


1387-1387: Logging statement uses %

(G002)


2228-2228: Abstract raise to an inner function

(TRY301)


2228-2228: Avoid specifying long messages outside the exception class

(TRY003)

🔇 Additional comments (8)
viewmaster/pdsgroup.py (3)

1-11: LGTM!

The module docstring clearly describes the purpose of the PdsGroup class and its role in Viewmaster rendering.


33-72: LGTM!

The __init__ method correctly handles mutable defaults by using None and initializing inside the function. The anchor parameter is now properly assigned to self.anchor. Based on learnings, the implementation uses Pds3File as intended.


558-598: LGTM!

The group_children static method correctly groups children by anchor and returns properly sorted PdsGroup objects.

viewmaster/viewmaster.py (5)

2138-2153: LGTM! Path traversal vulnerability properly addressed.

Using send_from_directory() correctly prevents path traversal attacks by safely joining paths internally. This addresses the security concern from the previous review.


80-93: LGTM! Explicit imports replace wildcard import.

The change from from .viewmaster_config import * to explicit named imports improves code clarity and eliminates static analysis warnings (F403/F405).


102-167: LGTM! Logger creation pattern is well-structured.

The separation of create_logger() as a pure factory and get_or_create_logger() as a singleton accessor is a clean design that prevents duplicate handlers and enables testing flexibility.


386-431: LGTM! Memcache fallback uses local variable correctly.

The UnboundLocalError issue is fixed by using a local memcache_port variable instead of reassigning the global VIEWMASTER_MEMCACHE_PORT.


2538-2547: Development-only debug mode acknowledged.

Per repository learnings, the debug=True in the __main__ block is intentionally kept for development. Production uses a WSGI server which bypasses this code path.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines 1383 to +1388
start_time = datetime.datetime.now()
(page['prev'], page['next']) = get_prev_next_navigation(query_pdsfile)
(page['prev'], page['next']) = get_prev_next_navigation(query_pdsfile, logger)
elapsed = (datetime.datetime.now() - start_time).total_seconds()
if elapsed > 10:
LOGGER.warn('Neighbor navigation took %.1f sec' % elapsed,
query_pdsfile.abspath)
logger.warning('Neighbor navigation took %.1f sec' % elapsed,
query_pdsfile.abspath)
Copy link

@coderabbitai coderabbitai bot Jan 14, 2026

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider adding timezone to datetime.now() call.

datetime.datetime.now() is called without a timezone argument, which could cause issues in timezone-aware environments. Consider using datetime.datetime.now(datetime.timezone.utc) for consistency.

♻️ Suggested improvement
+import datetime
+
     # Get neighbor navigation and warn about timing if it is very slow
-    start_time = datetime.datetime.now()
+    start_time = datetime.datetime.now(datetime.timezone.utc)
     (page['prev'], page['next']) = get_prev_next_navigation(query_pdsfile, logger)
-    elapsed = (datetime.datetime.now() - start_time).total_seconds()
+    elapsed = (datetime.datetime.now(datetime.timezone.utc) - start_time).total_seconds()

This pattern should also be applied to the other datetime.datetime.now() calls in viewmaster() and directory_page_html().

🧰 Tools
🪛 Ruff (0.14.11)

1383-1383: datetime.datetime.now() called without a tz argument

(DTZ005)


1385-1385: datetime.datetime.now() called without a tz argument

(DTZ005)


1387-1387: Use format specifiers instead of percent format

Replace with format specifiers

(UP031)


1387-1387: Logging statement uses %

(G002)

🤖 Prompt for AI Agents
In `@viewmaster/viewmaster.py` around lines 1383 - 1388, The timestamp uses naive
datetime (start_time = datetime.datetime.now()) which can cause timezone issues;
replace calls to datetime.datetime.now() with timezone-aware calls (e.g.,
datetime.datetime.now(datetime.timezone.utc)) in this block around start_time
and elapsed calculation and similarly update other occurrences in viewmaster()
and directory_page_html(); ensure get_prev_next_navigation timing and the
logger.warning call still reference the new start_time/elapsed variables and
that imported datetime.timezone is available.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

leave as-is

Copy link

Choose a reason for hiding this comment

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

@juzen2003, understood! I'll leave the datetime.datetime.now() calls as-is without timezone arguments.


🧠 Learnings used
Learnt from: juzen2003
Repo: SETI/rms-viewmaster PR: 14
File: README.md:16-18
Timestamp: 2026-01-12T21:03:22.730Z
Learning: In the rms-viewmaster repository, the README.md format should match the README format used in other SETI repositories (such as rms-starcat) for consistency, even if it results in MD031 linting violations (missing blank lines around fenced code blocks).

Updated the fill_page_cache function to declare PAGE_CACHE as a global variable, ensuring it is properly referenced within the function. This change enhances the clarity of cache management and maintains consistency in variable usage across the module.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (2)
viewmaster/viewmaster.py (2)

666-684: Critical bug: row_pdsfile is referenced but never defined.

Line 673 references row_pdsfile, but this variable is never defined in get_prev_next_navigation(). This will cause a NameError at runtime when processing a nonexistent index row.

Based on the context, it appears row_pdsfile should likely be derived from query_pdsfile or obtained through some other means before being used.

Likely fix (needs verification)
     # Deal with a nonexistent index row--it still has neighbors!
     if query_pdsfile.is_index_row and not query_pdsfile.exists:

         # Note that, in this case, each list begins with query_pdsfile, but
         # that row_pdsfile is missing from both lists. We need to put it back
         # in the proper place

-        row_pdsfile = row_pdsfile.copy()
+        # If the intent is to work with query_pdsfile:
+        row_pdsfile = query_pdsfile.copy()
         row_pdsfile.nav_name = row_pdsfile.basename
         row_pdsfile.division = False
         row_pdsfile.terminated = False

Please verify the intended behavior and the correct source for row_pdsfile.


2211-2213: Minor: Redundant exception object in logger.exception() call.

Line 2212 passes the exception object e to logger.exception(), but exception() automatically captures exception info. This is redundant but not harmful.

Optional cleanup
         except Exception as e:
-            LOGGER.exception(e)
+            LOGGER.exception('Cache retrieval failed')
             pass
🤖 Fix all issues with AI agents
In `@viewmaster/viewmaster.py`:
- Line 33: Replace the deprecated cgi usage: remove the "import cgi" and add
"import html" at the top of viewmaster.py, then replace all calls to cgi.escape
with html.escape (preserve the existing second arg/quote behavior, e.g.
cgi.escape(s, quote=True) -> html.escape(s, quote=True)); specifically update
the cgi.escape occurrences noted around lines 1106, 2039, and 2052 to use
html.escape.
- Around line 2376-2388: The function fill_page_cache(logger) declares a logger
parameter but never uses it, causing an API inconsistency; either remove the
unused logger parameter from fill_page_cache and update all call sites that pass
a logger to call fill_page_cache() with no args, or keep the parameter and
replace internal uses of the global LOGGER/viewmaster() logging with direct
calls to the passed-in logger (e.g., logger.info/error) inside fill_page_cache;
also ensure any callers that expect the old signature continue to pass or stop
passing the logger to match your chosen change.
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7a07718 and d5521a9.

📒 Files selected for processing (1)
  • viewmaster/viewmaster.py
🧰 Additional context used
🧠 Learnings (5)
📚 Learning: 2026-01-12T22:07:33.958Z
Learnt from: juzen2003
Repo: SETI/rms-viewmaster PR: 14
File: viewmaster/viewmaster.py:2496-2501
Timestamp: 2026-01-12T22:07:33.958Z
Learning: In viewmaster/viewmaster.py, the if __name__ == '__main__': block that sets debug=True is for development only and should not run in production. Ensure production deployment uses the WSGI server path and that any debug execution is removed or guarded (e.g., do not enable debug mode in the main entry point for production).

Applied to files:

  • viewmaster/viewmaster.py
📚 Learning: 2026-01-12T21:03:22.730Z
Learnt from: juzen2003
Repo: SETI/rms-viewmaster PR: 14
File: README.md:16-18
Timestamp: 2026-01-12T21:03:22.730Z
Learning: In the rms-viewmaster repository, the README.md format should match the README format used in other SETI repositories (such as rms-starcat) for consistency, even if it results in MD031 linting violations (missing blank lines around fenced code blocks).

Applied to files:

  • viewmaster/viewmaster.py
📚 Learning: 2025-12-12T23:26:32.767Z
Learnt from: juzen2003
Repo: SETI/rms-viewmaster PR: 14
File: docs/Makefile:11-20
Timestamp: 2025-12-12T23:26:32.767Z
Learning: In the rms-viewmaster repository, the `docs/Makefile` is auto-generated by Sphinx and should not be modified, even if linters flag missing standard phony targets.

Applied to files:

  • viewmaster/viewmaster.py
📚 Learning: 2025-12-18T21:42:57.270Z
Learnt from: juzen2003
Repo: SETI/rms-viewmaster PR: 14
File: viewmaster/pdsgrouptable.py:21-22
Timestamp: 2025-12-18T21:42:57.270Z
Learning: In viewmaster, use `pdsfile.Pds3File` explicitly in code implementation instead of the generic `pdsfile.PdsFile` base class in pdsgrouptable.py and pdsiterator.py. Docstrings should reference `Pds3File`, with `Pds4File` to be added later when PDS4 is fully supported.

Applied to files:

  • viewmaster/viewmaster.py
📚 Learning: 2026-01-12T21:13:24.268Z
Learnt from: juzen2003
Repo: SETI/rms-viewmaster PR: 14
File: viewmaster/pdsgrouptable.py:460-467
Timestamp: 2026-01-12T21:13:24.268Z
Learning: In viewmaster's pdsgrouptable.py `tables_from_pdsfiles` and similar methods, when a path string cannot be resolved as either Pds3File or Pds4File, the program should be allowed to crash rather than silently skipping the file. This fail-fast approach is preferred for invalid path inputs.

Applied to files:

  • viewmaster/viewmaster.py
🪛 ast-grep (0.40.5)
viewmaster/viewmaster.py

[warning] 2546-2546: Detected Flask app with debug=True. Do not deploy to production with this flag enabled as it will leak sensitive information. Instead, consider using Flask configuration variables or setting 'debug' using system environment variables.
Context: app.run(host='0.0.0.0', port=8080, debug=True)
Note: [CWE-489] Active Debug Code. [REFERENCES]
- https://labs.detectify.com/2015/10/02/how-patreon-got-hacked-publicly-exposed-werkzeug-debugger/

(debug-enabled-python)

🪛 Ruff (0.14.11)
viewmaster/viewmaster.py

1-1: The file is executable but no shebang is present

(EXE002)


96-96: Boolean positional value in function call

(FBT003)


133-133: Logging statement uses f-string

(G004)


144-144: Logging statement uses f-string

(G004)


263-263: Avoid specifying long messages outside the exception class

(TRY003)


346-346: Avoid specifying long messages outside the exception class

(TRY003)


408-409: Use format specifiers instead of percent format

Replace with format specifiers

(UP031)


408-409: Logging statement uses %

(G002)


416-417: Use format specifiers instead of percent format

Replace with format specifiers

(UP031)


416-417: Logging statement uses %

(G002)


439-439: Boolean default positional argument in function definition

(FBT002)


1385-1385: datetime.datetime.now() called without a tz argument

(DTZ005)


1387-1387: Use format specifiers instead of percent format

Replace with format specifiers

(UP031)


1387-1387: Logging statement uses %

(G002)


2228-2228: Abstract raise to an inner function

(TRY301)


2228-2228: Avoid specifying long messages outside the exception class

(TRY003)


2376-2376: Unused function argument: logger

(ARG001)

🔇 Additional comments (5)
viewmaster/viewmaster.py (5)

102-167: LGTM!

The logger creation pattern is well-structured with proper separation between the factory (create_logger()) and singleton accessor (get_or_create_logger()). The duplicate handler protection via has_handlers check and graceful error handling for file operations are appropriate.


249-263: LGTM!

The environment variable-based holdings path retrieval is cleaner than the previous config file approach. Clear error messaging when PDS3_HOLDINGS_DIR is not set.


386-431: LGTM!

The cache initialization correctly uses a local memcache_port variable to avoid the previous UnboundLocalError issue. The fallback from MemcachedCache to DictionaryCache on connection failure provides good resilience.


2538-2547: LGTM!

The main entry point correctly initializes the logger, loads icons, sets up holdings paths and page cache, then initializes caches before starting the app. The debug=True is acceptable for development-only execution. Based on learnings, production deployment uses a WSGI server rather than this entry point.


2138-2153: LGTM!

The path traversal vulnerability has been correctly addressed by using send_from_directory() instead of manually constructing file paths. Flask's send_from_directory() internally validates that the resolved path stays within the specified root directory.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Updated the code to replace cgi.escape with html.escape for escaping parameters in the directory_page_html and url_params functions. This change improves security and aligns with modern Python practices for handling HTML content.
Updated the fill_page_cache function to eliminate the logger parameter, simplifying its signature. This change enhances the function's usability and aligns with the current logging practices in the module.
Copy link

@coderabbitai coderabbitai bot left a comment

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 (4)
viewmaster/viewmaster.py (4)

454-456: Remove debug print statement.

Line 455 contains a print statement that appears to be a debug artifact. It should either be removed or converted to a proper logging call.

🔧 Proposed fix
     LOGGER.replace_root(HOLDINGS_PATHS)
-    print(VIEWMASTER_PREFIX_+ICON_URL_)
+    LOGGER.debug('Icon URL prefix: %s', VIEWMASTER_PREFIX_ + ICON_URL_)
     Pds3File.preload(HOLDINGS_PATHS, port=PDSFILE_MEMCACHE_PORT,
                      clear=reset, icon_url=ICON_URL_)

2211-2213: Redundant exception object in LOGGER.exception() call.

LOGGER.exception(e) on line 2212 passes the exception object unnecessarily. The exception() method automatically captures exception information from the current context.

♻️ Proposed fix
         except Exception as e:
-            LOGGER.exception(e)
+            LOGGER.exception('Cache retrieval failed')
             pass

1821-1832: RuntimeError: Dictionary modification during iteration.

Deleting keys from new_params while iterating over it will raise RuntimeError: dictionary changed size during iteration. Use a list comprehension or iterate over a copy.

🐛 Proposed fix
     # Remove unrecognized keys
     new_params = params.copy()
-    for key in new_params:
-        if key not in defaults:
-            del new_params[key]
+    new_params = {k: v for k, v in new_params.items() if k in defaults}

     # Fill in default values for missing keys
     for key in defaults:
         if key not in new_params:
             new_params[key] = defaults[key]

-    return clean_query_params(params)
+    return clean_query_params(new_params)

Note: Line 1832 also has a bug - it should pass new_params to clean_query_params, not the original params.


2064-2086: Fix WTForms validator signature to accept (form, field).

The pattern_validator function signature is incompatible with WTForms. Validators in WTForms must accept two arguments: (form, field). The current signature pattern_validator(field) will cause a TypeError at runtime when form validation is triggered, as WTForms will pass both the form and field but the validator only accepts one parameter.

Update the signature to def pattern_validator(form, field): at line 2064.

🤖 Fix all issues with AI agents
In `@viewmaster/viewmaster.py`:
- Around line 380-381: Replace the fragile assertion with explicit validation:
instead of using "assert len(paths) == 1", check "if len(paths) != 1:" and raise
a clear exception (e.g., ValueError("expected exactly one path in paths, got
{len(paths)}")) or log an error and exit; then assign HOLDINGS_PATHS = paths
only after the check succeeds. Ensure you update the code around the symbols
"paths" and "HOLDINGS_PATHS" and remove the assert so the validation runs even
with Python -O.
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d5521a9 and 10eb749.

📒 Files selected for processing (1)
  • viewmaster/viewmaster.py
🧰 Additional context used
🧠 Learnings (5)
📚 Learning: 2026-01-12T22:07:33.958Z
Learnt from: juzen2003
Repo: SETI/rms-viewmaster PR: 14
File: viewmaster/viewmaster.py:2496-2501
Timestamp: 2026-01-12T22:07:33.958Z
Learning: In viewmaster/viewmaster.py, the if __name__ == '__main__': block that sets debug=True is for development only and should not run in production. Ensure production deployment uses the WSGI server path and that any debug execution is removed or guarded (e.g., do not enable debug mode in the main entry point for production).

Applied to files:

  • viewmaster/viewmaster.py
📚 Learning: 2026-01-12T21:03:22.730Z
Learnt from: juzen2003
Repo: SETI/rms-viewmaster PR: 14
File: README.md:16-18
Timestamp: 2026-01-12T21:03:22.730Z
Learning: In the rms-viewmaster repository, the README.md format should match the README format used in other SETI repositories (such as rms-starcat) for consistency, even if it results in MD031 linting violations (missing blank lines around fenced code blocks).

Applied to files:

  • viewmaster/viewmaster.py
📚 Learning: 2025-12-12T23:26:32.767Z
Learnt from: juzen2003
Repo: SETI/rms-viewmaster PR: 14
File: docs/Makefile:11-20
Timestamp: 2025-12-12T23:26:32.767Z
Learning: In the rms-viewmaster repository, the `docs/Makefile` is auto-generated by Sphinx and should not be modified, even if linters flag missing standard phony targets.

Applied to files:

  • viewmaster/viewmaster.py
📚 Learning: 2025-12-18T21:42:57.270Z
Learnt from: juzen2003
Repo: SETI/rms-viewmaster PR: 14
File: viewmaster/pdsgrouptable.py:21-22
Timestamp: 2025-12-18T21:42:57.270Z
Learning: In viewmaster, use `pdsfile.Pds3File` explicitly in code implementation instead of the generic `pdsfile.PdsFile` base class in pdsgrouptable.py and pdsiterator.py. Docstrings should reference `Pds3File`, with `Pds4File` to be added later when PDS4 is fully supported.

Applied to files:

  • viewmaster/viewmaster.py
📚 Learning: 2026-01-12T21:13:24.268Z
Learnt from: juzen2003
Repo: SETI/rms-viewmaster PR: 14
File: viewmaster/pdsgrouptable.py:460-467
Timestamp: 2026-01-12T21:13:24.268Z
Learning: In viewmaster's pdsgrouptable.py `tables_from_pdsfiles` and similar methods, when a path string cannot be resolved as either Pds3File or Pds4File, the program should be allowed to crash rather than silently skipping the file. This fail-fast approach is preferred for invalid path inputs.

Applied to files:

  • viewmaster/viewmaster.py
🪛 ast-grep (0.40.5)
viewmaster/viewmaster.py

[warning] 2543-2543: Detected Flask app with debug=True. Do not deploy to production with this flag enabled as it will leak sensitive information. Instead, consider using Flask configuration variables or setting 'debug' using system environment variables.
Context: app.run(host='0.0.0.0', port=8080, debug=True)
Note: [CWE-489] Active Debug Code. [REFERENCES]
- https://labs.detectify.com/2015/10/02/how-patreon-got-hacked-publicly-exposed-werkzeug-debugger/

(debug-enabled-python)

🪛 Ruff (0.14.11)
viewmaster/viewmaster.py

1-1: The file is executable but no shebang is present

(EXE002)


96-96: Boolean positional value in function call

(FBT003)


133-133: Logging statement uses f-string

(G004)


144-144: Logging statement uses f-string

(G004)


263-263: Avoid specifying long messages outside the exception class

(TRY003)


346-346: Avoid specifying long messages outside the exception class

(TRY003)


408-409: Use format specifiers instead of percent format

Replace with format specifiers

(UP031)


408-409: Logging statement uses %

(G002)


416-417: Use format specifiers instead of percent format

Replace with format specifiers

(UP031)


416-417: Logging statement uses %

(G002)


439-439: Boolean default positional argument in function definition

(FBT002)


1385-1385: datetime.datetime.now() called without a tz argument

(DTZ005)


1387-1387: Use format specifiers instead of percent format

Replace with format specifiers

(UP031)


1387-1387: Logging statement uses %

(G002)


2228-2228: Abstract raise to an inner function

(TRY301)


2228-2228: Avoid specifying long messages outside the exception class

(TRY003)

🔇 Additional comments (8)
viewmaster/viewmaster.py (8)

20-56: LGTM! Well-organized imports.

The import reorganization is clean:

  • Flask imports properly grouped with send_from_directory for secure file serving
  • html module correctly replaces the deprecated cgi module
  • Relative imports (.pdsgroup, .pdsgrouptable, .pdsiterator) are appropriate for package structure

80-100: LGTM! Explicit imports and lazy initialization pattern.

The explicit imports from viewmaster_config eliminate static analysis warnings and make dependencies clear. The global variables (LOGGER, HOLDINGS_PATHS, PAGE_CACHE) are properly initialized to None for lazy initialization.


102-167: LGTM! Clean logger implementation with proper safeguards.

The logger implementation properly addresses previous concerns:

  • has_handlers check (line 122) prevents duplicate handlers on logger reuse
  • OSError handling (lines 132-133, 143-144) prevents crashes on file-open failures
  • get_or_create_logger() provides a clean singleton pattern

249-348: LGTM! Holdings path validation with consistent error handling.

The implementation correctly:

  • Uses OSError consistently (replacing deprecated IOError)
  • Accepts logger as a parameter for explicit dependency injection
  • Validates holdings paths with appropriate logging for various failure modes

573-588: LGTM! Consistent logger parameter threading.

The logger is properly passed as a parameter through the rendering function chain (get_prev_next_navigation, get_directory_page, directory_page_html, get_product_page_info, product_page_html), enabling explicit dependency injection and avoiding reliance on global state within these functions.


2148-2153: LGTM! Path traversal vulnerability fixed.

Using send_from_directory() properly prevents path traversal attacks by safely joining the root directory with the query path.


2376-2434: LGTM! Cache warming implementation.

The fill_page_cache() function properly uses the global PAGE_CACHE and walks the holdings tree to warm commonly accessed pages.


2535-2544: LGTM! Development entry point with proper initialization.

The __main__ block correctly sequences initialization: logger → icons → holdings path → page cache → caches → app start. Based on learnings, debug=True is intentional for development use only, as production deployment uses a WSGI server that bypasses this code path.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines 380 to 381
assert len(paths) == 1
HOLDINGS_PATHS = paths
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider replacing assert with explicit validation.

The assert statement can be disabled when Python runs with the -O (optimize) flag, which would silently skip this check in production. For critical validation, use an explicit check:

♻️ Proposed fix
-    assert len(paths) == 1
-    HOLDINGS_PATHS = paths
+    if len(paths) != 1:
+        raise RuntimeError(f'Expected exactly one holdings path, got {len(paths)}')
+    HOLDINGS_PATHS = paths
🤖 Prompt for AI Agents
In `@viewmaster/viewmaster.py` around lines 380 - 381, Replace the fragile
assertion with explicit validation: instead of using "assert len(paths) == 1",
check "if len(paths) != 1:" and raise a clear exception (e.g.,
ValueError("expected exactly one path in paths, got {len(paths)}")) or log an
error and exit; then assign HOLDINGS_PATHS = paths only after the check
succeeds. Ensure you update the code around the symbols "paths" and
"HOLDINGS_PATHS" and remove the assert so the validation runs even with Python
-O.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed.

Modified the get_holdings_path function to raise a RuntimeError if the number of holdings paths is not exactly one, improving error handling and clarity in path validation. This change enhances the robustness of the function by ensuring it meets expected conditions before proceeding.
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.

3 participants