Skip to content

Clean up viewmaster - #14

Open
juzen2003 wants to merge 132 commits into
mainfrom
clean_up_viewmaster
Open

Clean up viewmaster #14
juzen2003 wants to merge 132 commits into
mainfrom
clean_up_viewmaster

Conversation

@juzen2003

@juzen2003 juzen2003 commented Sep 29, 2025

Copy link
Copy Markdown
Collaborator

Fixes #3. Progresses #9. Progresses #12.

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.
  • 03/11/26:
    • Fixed all the review comments from Rob.
    • Add documents on how pdsfile rules affect the viewmaster .
  • 08/31/26:
    • Cleanup based on the review feedback:
      • WSGI startup: create_app() / init_once() now load icons and run initialize_caches() (with an idempotency guard), so the Apache/WSGI path works again—not only python -m viewmaster.viewmaster.
      • Safer local run: default bind is 127.0.0.1, debug=False, host/port via env; removed leftover print(); holdings init re-raises instead of sys.exit.
      • Config / Clean up Viewmaster so it's easier to use by developers #9 ergonomics: dropped dead MAKE_SYMLINKS / WEBSITE_ROOT_; Linux testing mode keeps memcache disabled; VIEWMASTER_TESTING is env-driven (with a narrow CLI fallback); production paths/URLs overridable via env; local logs no longer need sudo under /var/www.
      • Link service: no runtime holdings* symlink glob—resolves files via PDS3_HOLDINGS_DIR.
        Secrets: VIEWMASTER_SECRET_KEY required in production (dev fallback only when testing).
      • Docs / Write overall documentation #12: Apache/WSGI deploy notes + example conf (incl. venv python-home); commented out broken PyPI/CI/codecov badges; ReadTheDocs link points at /en/latest/.

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

    • Improved logging and cache initialization for more reliable startup and diagnostics.
    • New grouping and iterator utilities for richer directory/file navigation and display.
  • Documentation

    • Full Sphinx docs added; README expanded and ReadTheDocs/config tooling included.
  • Refactor

    • Rendering and request flows now thread logging/context for clearer tracing.
  • 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

coderabbitai Bot commented Sep 29, 2025

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Team

Run ID: 984cb512-dfa3-4779-9ca4-89bf610a6ab6

Note

Reviews paused

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

Use the following commands to manage reviews:

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

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Centralized logging and app initialization: added create_logger/get_or_create_logger, init_once/create_app and blueprints; threaded logger through page-rendering/navigation APIs; refactored link into a blueprint; added iterators and richer PdsGroup/PdsGroupTable APIs; removed wsgi_init; added Sphinx docs, README, and packaging-related changes.

Changes

Cohort / File(s) Summary
Core app, logging & init
viewmaster/viewmaster.py
Add create_logger() / get_or_create_logger(), init_once(), create_app(); replace module-global LOGGER with passed logger; make cache/holdings init logger-aware; convert app startup to factory pattern.
Blueprints & WSGI entrypoints
viewmaster/link.py, link.wsgi, viewmaster.wsgi
Convert link to link_bp Blueprint and add create_app(); change WSGI entrypoints to call create_app(); replace module-level app imports with factory usage.
Routing & handlers
viewmaster/viewmaster.py, viewmaster/link.py
Thread logger into route handlers and page assembly functions; update route decorators to use blueprint; update signatures for directory/product/navigation functions to accept logger.
Configuration
viewmaster/viewmaster_config.py
Simplify testing detection to sys.argv[0].endswith('viewmaster.py'); remove VIEWMASTER_FOR_MARK and HTTPD_CUSTOMIZATION; adjust memcache socket/port defaults and testing overrides; add module docstring.
Page APIs & rendering
viewmaster/...
Update get_directory_page, directory_page_html, get_product_page_info, product_page_html, get_prev_next_navigation, and cache helpers to accept logger; replace direct LOGGER calls with logger methods.
Grouping & tables
viewmaster/pdsgroup.py, viewmaster/pdsgrouptable.py
Refactor/add PdsGroup and enhance PdsGroupTable: safer None defaults, richer docstrings, new/changed constructors, merge/insert/remove/hide APIs, sort and visibility helpers, new methods (group_children, remove_hidden, merge_index_row_tables).
Iterators
viewmaster/pdsiterator.py
Add PdsDirIterator, PdsFileIterator, PdsRowIterator, and dirs_only() with forward/backward traversal, cousin navigation, filtering, and copy semantics.
Docs & CI config
.readthedocs.yaml, docs/conf.py, docs/*.rst, docs/Makefile, docs/make.bat
Add Read the Docs config and Sphinx scaffolding, conf, multiple automodule RST files, and Makefile / Windows batch for building docs.
Project docs & deps
README.md, requirements.txt
Replace README with expanded documentation and add Sphinx-related dependencies (myst-parser, sphinx, sphinxcontrib-napoleon, sphinx-rtd-theme).
Removed helper
wsgi_init.py
Remove legacy WSGI environment bootstrap helper; ensure WSGI initialization now via app factories.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'Clean up viewmaster' is vague and generic, using non-descriptive language that doesn't clearly convey the main changes. Provide a more specific title that highlights key changes, such as: 'Refactor logger initialization, add Sphinx documentation, and remove Apache dependencies' or 'Add documentation and refactor app initialization with logger propagation'.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR addresses all three linked issues: Issue #3 (holdings path and Python version fixes via environment variables), Issue #9 (removing Apache dependencies and Flask refactoring), and Issue #12 (com…
Out of Scope Changes check ✅ Passed All changes are in-scope with linked issues; no unrelated modifications detected. Changes include logger refactoring, Flask blueprints, documentation (README/Sphinx), removal of wsgi_init.py, and conf…
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Full details: Linked Issues check

Explanation

The PR addresses all three linked issues: Issue #3 (holdings path and Python version fixes via environment variables), Issue #9 (removing Apache dependencies and Flask refactoring), and Issue #12 (comprehensive documentation via README and Sphinx).

Full details: Out of Scope Changes check

Explanation

All changes are in-scope with linked issues; no unrelated modifications detected. Changes include logger refactoring, Flask blueprints, documentation (README/Sphinx), removal of wsgi_init.py, and configuration updates for environment variables.


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

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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

Comment thread viewmaster/doc/viewmaster_v1.md Outdated
Comment thread viewmaster/doc/viewmaster_v1.md Outdated
- `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
Copy Markdown

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 thread viewmaster/doc/viewmaster_v1.md Outdated
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
Copy Markdown

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 thread viewmaster/viewmaster_config.py Outdated
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]

@coderabbitai coderabbitai Bot Sep 29, 2025

Copy link
Copy Markdown

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
Copy Markdown
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
Copy Markdown

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.

(„• ֊ •„)
づ♡

Comment thread viewmaster/viewmaster_without_pause.py Outdated
Comment thread viewmaster/viewmaster.py Outdated
…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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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 thread README.md
Comment thread viewmaster/doc/viewmaster_v1.md Outdated
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
Copy Markdown

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.

Comment thread viewmaster/doc/viewmaster_v1.md Outdated
pip install -r requirements.txt
```

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

Copy link
Copy Markdown

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".

Comment thread viewmaster/doc/viewmaster_v1.md Outdated

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

Copy link
Copy Markdown

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 thread viewmaster/doc/viewmaster_v1.md Outdated
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
Copy Markdown

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
Copy Markdown
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
Copy Markdown
Collaborator

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

juzen2003 added 16 commits March 5, 2026 23:17
…blank line before conditional check in PdsGroup class for improved readability.
Inserted blank lines before method definitions and within methods in the PdsGroupTable class to enhance code readability and maintainability.
…erator, and PdsRowIterator classes

Inserted blank lines before method definitions and within methods in the PdsDirIterator, PdsFileIterator, and PdsRowIterator classes to enhance code readability and maintainability.
Inserted blank lines before method definitions and within methods in viewmaster.py to enhance code readability and maintainability.
…rator, PdsFileIterator, and PdsRowIterator classes. Updated calls to use __next__() directly, enhancing code clarity and consistency with Python 3 standards.
Inserted a blank line before the code block in the Running Locally section of README.md to enhance clarity and maintainability of the documentation.
Revised the docstring in the get_holdings_paths function to specify the full name of the environment variable as "PDS3_HOLDINGS_DIR". This change enhances the clarity and accuracy of the documentation.
… and Mac

Expanded the instructions for creating the necessary web server directories by specifying the commands for both Linux and Mac environments. This change aims to improve clarity and ensure users avoid permission issues when setting up their environment.
…wmaster.py

Replaced direct app instantiation with a create_app function in both link.py and viewmaster.py. This change centralizes the app creation process and initializes the logger and other global variables in a dedicated init_once function, improving code organization and maintainability.
Updated import statements in link.wsgi and viewmaster.wsgi to reflect the new module structure. Removed the wsgi_init.py file as it is no longer needed, streamlining the application setup process.
Updated link.wsgi to use a create_app function for application instantiation. Refactored link.py to define a Blueprint for routing, enhancing modularity and organization of the Flask application. This change improves the overall structure and maintainability of the code.
Updated viewmaster.wsgi to utilize a create_app function for application initialization. Refactored viewmaster.py to register the viewmaster Blueprint for routing, enhancing modularity and organization. This change improves the maintainability and clarity of the application structure.
…and viewmaster.py

Updated the docstrings for the create_app function in both link.py and viewmaster.py to provide detailed descriptions of the application initialization process and the registered blueprints. Added a docstring for the init_once function in viewmaster.py to clarify its role in setting up global module state. These changes improve code documentation and maintainability.
…EADME.md

Expanded the README.md to include a comprehensive overview of the Viewmaster and PdsFile rules interface. Added sections on rendering architecture, main file table, related files table, documentation table, info section, and neighbor navigation, along with associated rules and rendering details. Included new images for better visualization of the tables and navigation elements.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Caution

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

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

351-404: ⚠️ Potential issue | 🔴 Critical

Remove duplicate sort method definition.

The sort method is defined twice: once at lines 297-349 and again at lines 351-404. The second definition shadows the first. Both implementations appear identical, so the duplicate should be removed.

🐛 Proposed fix: remove duplicate method
         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.
-
-        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 append(self, pdsf, hidden=False):
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@viewmaster/pdsgroup.py` around lines 351 - 404, There are two identical
definitions of the method sort in the class (one earlier and one later) causing
the latter to shadow the former; remove the duplicate definition block (the
second sort method that contains basename_dict, parent_pdsf handling, label
adjacency logic and final self.rows reassignment) so only a single sort method
remains, preserving the existing implementation that references basename_dict,
parent_pdsf, basename_is_label, sort_basenames and self.rows.

326-326: 🧹 Nitpick | 🔵 Trivial

Inconsistent indentation: 2 spaces instead of 4.

The nested for loop body uses 2-space indentation instead of the standard 4 spaces used elsewhere in the file. This is syntactically valid but inconsistent.

             if len(labels) > 1:
-              for label in labels:
-                l = sorted.index(label)                     # index of label
+                for label in labels:
+                    l = sorted.index(label)                 # index of label
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@viewmaster/pdsgroup.py` at line 326, The nested loop starting with the
statement "for label in labels:" has its body indented with 2 spaces instead of
the project's standard 4-space indentation; update the body of the "for label in
labels:" loop to use 4-space indentation to match the rest of the file (ensure
any nested statements inside that loop such as subsequent statements or inner
loops are adjusted as well) and run the project's linter/formatter to verify
consistent indentation across the function containing that loop.
viewmaster/pdsiterator.py (2)

276-276: 🧹 Nitpick | 🔵 Trivial

Typo in comment: "basenam" should be "basename".

-        # the pattern applies to the basenam
+        # the pattern applies to the basename
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@viewmaster/pdsiterator.py` at line 276, Fix the typo in the inline comment on
the line containing "# the pattern applies to the basenam" by changing "basenam"
to "basename" so the comment reads "# the pattern applies to the basename";
locate this comment in viewmaster/pdsiterator.py (around the block handling
pattern matching) and update the text accordingly.

81-91: ⚠️ Potential issue | 🔴 Critical

Missing early return causes NameError when pdsf is None.

When pdsf is None, lines 82-85 initialize an empty iterator, but execution continues to lines 87-91 which call isinstance(pdsf, ...) on None. This will not raise an error immediately, but pdsf_cls will never be assigned (neither condition matches), causing NameError on line 102.

🐛 Proposed fix: add early return
         if pdsf is None:
             self.neighbors = []
             self.neighbor_index = 0
             self.current_logical_path = None
             self.sign = 1
+            return

         if isinstance(pdsf, pdsfile.Pds3File):
             pdsf_cls = pdsfile.Pds3File
         elif isinstance(pdsf, pdsfile.Pds4File):
             pdsf_cls = pdsfile.Pds4File
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@viewmaster/pdsiterator.py` around lines 81 - 91, When pdsf is None the code
initializes the iterator state (self.neighbors, self.neighbor_index,
self.current_logical_path, self.sign) but continues and later tries to use
pdsf_cls, causing a NameError; fix this by adding an early return immediately
after those initializations so the function exits when pdsf is None and skips
the later isinstance checks and use of pdsf_cls (look for the block that sets
self.neighbors / self.neighbor_index / self.current_logical_path / self.sign and
add a return there).
viewmaster/viewmaster.py (1)

1797-1832: ⚠️ Potential issue | 🟠 Major

get_query_params_from_dict() is broken for extra keys.

Deleting from new_params inside for key in new_params raises RuntimeError: dictionary changed size during iteration, and the final clean_query_params(params) throws away the filtered/defaulted dict anyway. Build a new dict and return clean_query_params(new_params).

🐛 Proposed fix
-    new_params = params.copy()
-    for key in new_params:
-        if key not in defaults:
-            del new_params[key]
+    new_params = {
+        key: value for key, value in params.items() if key in defaults
+    }
@@
-    return clean_query_params(params)
+    return clean_query_params(new_params)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@viewmaster/viewmaster.py` around lines 1797 - 1832,
get_query_params_from_dict currently mutates new_params during iteration causing
a RuntimeError and then calls clean_query_params(params) which ignores the
filtered/defaulted dict; fix by constructing a new dict (e.g., start from
defaults and update with only allowed keys from the incoming params) instead of
deleting while iterating, then call and return clean_query_params(new_params) so
the cleaned/typed values are based on the merged defaults and allowed input
keys; adjust references to defaults, new_params, and clean_query_params in
get_query_params_from_dict accordingly.
♻️ Duplicate comments (1)
viewmaster/viewmaster.py (1)

2541-2560: ⚠️ Potential issue | 🟠 Major

Keep WSGI and __main__ on the same startup path.

create_app() is now the production entry point, but pdsviewable.load_icons(...) and initialize_caches(reset=False) still only run in __main__. A WSGI deployment will therefore skip icon loading, LOGGER.replace_root(), and Pds3File.preload(), so production boots with different global state than local runs.

♻️ Proposed fix
+INITIALIZED = False
+
 def init_once():
@@
-    global LOGGER, HOLDINGS_PATHS, PAGE_CACHE
+    global LOGGER, HOLDINGS_PATHS, PAGE_CACHE, INITIALIZED
+    if INITIALIZED:
+        return
     logger = get_or_create_logger()
     HOLDINGS_PATHS = get_holdings_path(logger)
     PAGE_CACHE = get_page_cache(logger)
+    pdsviewable.load_icons(path=ICON_ROOT_, url=ICON_URL_, color=ICON_COLOR,
+                           logger=logger)
+    initialize_caches(reset=False)
+    INITIALIZED = True
@@
 if __name__ == "__main__":
     app = create_app()
-    pdsviewable.load_icons(path=ICON_ROOT_, url=ICON_URL_, color=ICON_COLOR,
-                           logger=LOGGER)
-
-    initialize_caches(reset=False)
     app.run(host='0.0.0.0', port=8080, debug=True)

Also applies to: 2584-2589

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@viewmaster/viewmaster.py` around lines 2541 - 2560, The WSGI path skips
startup steps that only run in __main__, causing different global state; move
the icon loading, cache initialization, LOGGER.replace_root(), and
Pds3File.preload() into the shared startup invoked by both WSGI and __main__
(e.g., call pdsviewable.load_icons(...), initialize_caches(reset=False),
LOGGER.replace_root(), and Pds3File.preload() from init_once() or from
create_app() before returning the app) so create_app() and init_once() perform
the same initialization sequence and both entry points produce identical global
state.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@README.md`:
- Around line 252-260: The markdown table in README containing rows with
`page['tables']`, `page['associations']`, `page['documents']`, `page['info']`
and the header for Page Section / Page Key / Main Rules must be fixed by
normalizing the header separator length to match the table width, adding missing
trailing pipe characters at the end of each data row, and ensuring there is a
blank line before and after the table; update the separator line to a consistent
number of dashes, append a trailing `|` to lines that lack it (including the
rows containing `SORT_KEY`, `ASSOCIATIONS`, `associations_to_documents`,
`INFO_FILE_BASENAMES`, `NEIGHBORS`, `SIBLINGS`), and surround the entire table
with a blank line above and below so it renders consistently.

In `@requirements.txt`:
- Around line 1-13: The requirements.txt currently lists packages without
version constraints (flask, flask_wtf, myst-parser, psutil, pylibmc, pytest,
rms-pdsfile, rms-pdslogger, rms-pdstable, rms-translator, sphinx,
sphinxcontrib-napoleon, sphinx-rtd-theme); update this file to pin or bound
versions to ensure reproducible builds by either (a) running pip freeze and
replacing the file with the exact pins, or (b) adding conservative constraints
(e.g., package>=X.Y,<X+1) for each listed package—pay special attention to
sphinx, sphinxcontrib-napoleon and sphinx-rtd-theme—and commit the updated
requirements.txt so CI/ReadTheDocs uses fixed versions.
- Line 12: Remove the redundant dependency "sphinxcontrib-napoleon" from
requirements.txt; keep using the built-in extension 'sphinx.ext.napoleon'
already declared in docs/conf.py and then regenerate any lockfile or pinned
requirements (e.g., pip-compile / poetry lock / update requirements-dev) so
dependency metadata and CI install steps no longer include the removed package.

In `@viewmaster/pdsgrouptable.py`:
- Line 141: Replace the explicit list comprehension return [g for g in
self.groups] with the simpler and more idiomatic list(self.groups); locate the
method that returns the groups (the function containing the return statement
referencing self.groups in pdsgrouptable.py) and change the return to use
list(self.groups) to produce the same list more clearly and efficiently.
- Around line 446-448: The function tables_from_pdsfiles currently uses mutable
default arguments exclusions=set() and hidden=set(); change those defaults to
None in the signature and inside tables_from_pdsfiles initialize local empty
sets when they are None (e.g., if exclusions is None: exclusions = set(); if
hidden is None: hidden = set()), then continue to create copies where needed
(e.g., new_exclusions = set(exclusions)) to avoid shared mutable state; update
references to exclusions and hidden in the function body accordingly.

In `@viewmaster/viewmaster.py`:
- Around line 331-345: The validate_holdings_paths() loop currently appends
abspath even if one or more sibling dirs are missing; change the logic so that
you only append abspath to valid_abspaths when all three required siblings
('holdings','shelves','volinfo') exist and are directories: inside the for
dirname loop compute testpath = prefix_ + dirname, track a boolean (e.g.
valid=True) and set it False and break when os.path.exists(testpath) is False or
not os.path.isdir(testpath) (using the existing logger.warning/logger.error
calls), and after the loop only call valid_abspaths.append(abspath) if valid
remains True (do not append on partial failures).
- Around line 2575-2577: Replace the hard-coded Flask signing key by loading
VIEWMASTER_SECRET_KEY from the environment and fail-fast if it's not set: in the
place that creates the Flask app (the app = Flask(__name__) / create_app()
block) remove the literal app.secret_key assignment and instead read
os.environ['VIEWMASTER_SECRET_KEY'] (or os.getenv and raise if None) and assign
that value to app.secret_key or app.config['SECRET_KEY']; if the env var is
missing raise a RuntimeError with a clear message so the process exits
immediately.

---

Outside diff comments:
In `@viewmaster/pdsgroup.py`:
- Around line 351-404: There are two identical definitions of the method sort in
the class (one earlier and one later) causing the latter to shadow the former;
remove the duplicate definition block (the second sort method that contains
basename_dict, parent_pdsf handling, label adjacency logic and final self.rows
reassignment) so only a single sort method remains, preserving the existing
implementation that references basename_dict, parent_pdsf, basename_is_label,
sort_basenames and self.rows.
- Line 326: The nested loop starting with the statement "for label in labels:"
has its body indented with 2 spaces instead of the project's standard 4-space
indentation; update the body of the "for label in labels:" loop to use 4-space
indentation to match the rest of the file (ensure any nested statements inside
that loop such as subsequent statements or inner loops are adjusted as well) and
run the project's linter/formatter to verify consistent indentation across the
function containing that loop.

In `@viewmaster/pdsiterator.py`:
- Line 276: Fix the typo in the inline comment on the line containing "# the
pattern applies to the basenam" by changing "basenam" to "basename" so the
comment reads "# the pattern applies to the basename"; locate this comment in
viewmaster/pdsiterator.py (around the block handling pattern matching) and
update the text accordingly.
- Around line 81-91: When pdsf is None the code initializes the iterator state
(self.neighbors, self.neighbor_index, self.current_logical_path, self.sign) but
continues and later tries to use pdsf_cls, causing a NameError; fix this by
adding an early return immediately after those initializations so the function
exits when pdsf is None and skips the later isinstance checks and use of
pdsf_cls (look for the block that sets self.neighbors / self.neighbor_index /
self.current_logical_path / self.sign and add a return there).

In `@viewmaster/viewmaster.py`:
- Around line 1797-1832: get_query_params_from_dict currently mutates new_params
during iteration causing a RuntimeError and then calls
clean_query_params(params) which ignores the filtered/defaulted dict; fix by
constructing a new dict (e.g., start from defaults and update with only allowed
keys from the incoming params) instead of deleting while iterating, then call
and return clean_query_params(new_params) so the cleaned/typed values are based
on the merged defaults and allowed input keys; adjust references to defaults,
new_params, and clean_query_params in get_query_params_from_dict accordingly.

---

Duplicate comments:
In `@viewmaster/viewmaster.py`:
- Around line 2541-2560: The WSGI path skips startup steps that only run in
__main__, causing different global state; move the icon loading, cache
initialization, LOGGER.replace_root(), and Pds3File.preload() into the shared
startup invoked by both WSGI and __main__ (e.g., call
pdsviewable.load_icons(...), initialize_caches(reset=False),
LOGGER.replace_root(), and Pds3File.preload() from init_once() or from
create_app() before returning the app) so create_app() and init_once() perform
the same initialization sequence and both entry points produce identical global
state.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: cf7e81ab-110a-43a1-ae03-543d8d0c1df1

📥 Commits

Reviewing files that changed from the base of the PR and between 4d73629 and 724cb08.

⛔ Files ignored due to path filters (5)
  • README_icon/associations.png is excluded by !**/*.png
  • README_icon/documents.png is excluded by !**/*.png
  • README_icon/info.png is excluded by !**/*.png
  • README_icon/main_table.png is excluded by !**/*.png
  • README_icon/nav.png is excluded by !**/*.png
📒 Files selected for processing (10)
  • README.md
  • link.wsgi
  • requirements.txt
  • viewmaster.wsgi
  • viewmaster/link.py
  • viewmaster/pdsgroup.py
  • viewmaster/pdsgrouptable.py
  • viewmaster/pdsiterator.py
  • viewmaster/viewmaster.py
  • wsgi_init.py
💤 Files with no reviewable changes (1)
  • wsgi_init.py

Comment thread README.md Outdated
Comment thread requirements.txt Outdated
Comment on lines +1 to +13
flask
flask_wtf
myst-parser
psutil
pylibmc
pytest
rms-pdsfile
rms-pdslogger
rms-pdstable
rms-translator
sphinx
sphinxcontrib-napoleon
sphinx-rtd-theme

@coderabbitai coderabbitai Bot Mar 11, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider pinning dependency versions for reproducibility.

None of the dependencies have version constraints, which can lead to non-reproducible builds and potential breaking changes when packages are updated. Consider adding version pins or ranges, especially for the Sphinx-related packages.

💡 Example with version constraints

You could generate a pinned requirements file using:

pip freeze > requirements-pinned.txt

Or add minimum version constraints:

-sphinx
+sphinx>=7.0,<8.0
-sphinx-rtd-theme
+sphinx-rtd-theme>=2.0,<3.0

This helps ensure consistent builds across environments and Read the Docs deployments.

🧰 Tools
🪛 OSV Scanner (2.3.3)

[HIGH] 1-1: urllib3 1.26.20: urllib3 streaming API improperly handles highly compressed data

(GHSA-2xpw-w6gg-jr37)


[HIGH] 1-1: urllib3 1.26.20: Decompression-bomb safeguards bypassed when following HTTP redirects (streaming API)

(GHSA-38jv-5279-wg99)


[HIGH] 1-1: urllib3 1.26.20: urllib3 allows an unbounded number of links in the decompression chain

(GHSA-gm62-xv2j-4w53)


[HIGH] 1-1: urllib3 1.26.20: urllib3 redirects are not disabled when retries are disabled on PoolManager instantiation

(GHSA-pq67-6m6q-mj2v)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@requirements.txt` around lines 1 - 13, The requirements.txt currently lists
packages without version constraints (flask, flask_wtf, myst-parser, psutil,
pylibmc, pytest, rms-pdsfile, rms-pdslogger, rms-pdstable, rms-translator,
sphinx, sphinxcontrib-napoleon, sphinx-rtd-theme); update this file to pin or
bound versions to ensure reproducible builds by either (a) running pip freeze
and replacing the file with the exact pins, or (b) adding conservative
constraints (e.g., package>=X.Y,<X+1) for each listed package—pay special
attention to sphinx, sphinxcontrib-napoleon and sphinx-rtd-theme—and commit the
updated requirements.txt so CI/ReadTheDocs uses fixed versions.

Copy link
Copy Markdown
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
Copy Markdown

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 dependency versions unpinned.


✏️ Learnings added
Learnt from: juzen2003
Repo: SETI/rms-viewmaster PR: 14
File: requirements.txt:1-13
Timestamp: 2026-03-12T05:59:55.438Z
Learning: In the rms-viewmaster repository, the maintainer (juzen2003) intentionally keeps requirements.txt without version pins or constraints. Do not flag unpinned dependencies in requirements.txt for this repository.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
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.

Comment thread requirements.txt Outdated
Comment thread viewmaster/pdsgrouptable.py Outdated
Comment thread viewmaster/pdsgrouptable.py Outdated
Comment thread viewmaster/viewmaster.py
Comment thread viewmaster/viewmaster.py
Comment on lines +2575 to +2577
app = Flask(__name__)
app.secret_key = "Cassini Grand Finale!" # needed by flask_wtf
init_once()

@coderabbitai coderabbitai Bot Mar 11, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Does Flask use SECRET_KEY to sign session cookies, and does Flask-WTF use the same key for CSRF tokens?

💡 Result:

Yes.

  • Flask uses app.config["SECRET_KEY"] to cryptographically sign the session cookie (Flask’s default “secure cookie” session), so the client can’t tamper with session data without invalidating the signature. [1]
  • Flask-WTF uses a secret key for CSRF token signing/validation and, by default, it uses the Flask app’s SECRET_KEY. If you want a different key specifically for CSRF, set WTF_CSRF_SECRET_KEY. [2][3]

Sources:
[1] Flask docs – Configuration Handling (SECRET_KEY signs the session cookie) (flask.palletsprojects.com)
[2] Flask-WTF docs – CSRF Protection (WTF_CSRF_SECRET_KEY to use a separate key) (flask-wtf.readthedocs.io)
[3] Flask-WTF docs – Configuration (WTF_CSRF_SECRET_KEY setting) (flask-wtf.readthedocs.io)

Citations:


🏁 Script executed:

# First, let's check the file exists and get context around the specified lines
wc -l viewmaster/viewmaster.py

Repository: SETI/rms-viewmaster

Length of output: 92


🏁 Script executed:

# Read the relevant section with context
sed -n '2570,2585p' viewmaster/viewmaster.py

Repository: SETI/rms-viewmaster

Length of output: 531


🏁 Script executed:

# Search for all occurrences of SECRET_KEY or secret_key in the codebase
rg -i "secret.?key|create_app" viewmaster/viewmaster.py -B 2 -A 5

Repository: SETI/rms-viewmaster

Length of output: 896


🏁 Script executed:

# Check for environment variable usage patterns in the codebase
rg "os\.getenv|os\.environ|config" viewmaster/viewmaster.py | head -20

Repository: SETI/rms-viewmaster

Length of output: 614


🏁 Script executed:

# Check imports at the top of the file
head -50 viewmaster/viewmaster.py | grep -E "^import|^from"

Repository: SETI/rms-viewmaster

Length of output: 451


🏁 Script executed:

# Check if os is imported anywhere in the file
rg "^import os" viewmaster/viewmaster.py

Repository: SETI/rms-viewmaster

Length of output: 77


🏁 Script executed:

# Check how viewmaster_config.py is structured
head -50 viewmaster/viewmaster_config.py

Repository: SETI/rms-viewmaster

Length of output: 2396


🏁 Script executed:

# Check if there's already an environment variable for the secret key
rg "VIEWMASTER_SECRET_KEY|SECRET_KEY" viewmaster/ -i

Repository: SETI/rms-viewmaster

Length of output: 157


🏁 Script executed:

# Check deployment/production documentation
find . -name "*.md" -o -name "*.txt" -o -name "*.rst" | head -20

Repository: SETI/rms-viewmaster

Length of output: 284


🏁 Script executed:

# Look for any README or deployment docs
ls -la | grep -i readme

Repository: SETI/rms-viewmaster

Length of output: 184


Move Flask signing key to environment variable.

Hard-coded SECRET_KEY becomes the shared signing key for all deployments from create_app(). Anyone with source code access can forge signed session cookies and Flask-WTF CSRF tokens. Load from VIEWMASTER_SECRET_KEY environment variable and fail fast if missing.

Proposed fix
 def create_app():
     app = Flask(__name__)
-    app.secret_key = "Cassini Grand Finale!"    # needed by flask_wtf
+    secret_key = os.getenv('VIEWMASTER_SECRET_KEY')
+    if not secret_key:
+        raise RuntimeError('VIEWMASTER_SECRET_KEY must be set')
+    app.secret_key = secret_key
     init_once()
     app.register_blueprint(viewmaster_bp)
🧰 Tools
🪛 Ruff (0.15.5)

[error] 2576-2576: Possible hardcoded password assigned to: "secret_key"

(S105)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@viewmaster/viewmaster.py` around lines 2575 - 2577, Replace the hard-coded
Flask signing key by loading VIEWMASTER_SECRET_KEY from the environment and
fail-fast if it's not set: in the place that creates the Flask app (the app =
Flask(__name__) / create_app() block) remove the literal app.secret_key
assignment and instead read os.environ['VIEWMASTER_SECRET_KEY'] (or os.getenv
and raise if None) and assign that value to app.secret_key or
app.config['SECRET_KEY']; if the env var is missing raise a RuntimeError with a
clear message so the process exits immediately.

Copy link
Copy Markdown
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
Copy Markdown

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 secret_key as-is.


🧠 Learnings used
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).

…bility

Adjusted the layout of the summary table in the README.md to enhance clarity and organization. This change ensures that the information is presented in a more structured manner, making it easier for users to understand the main rules associated with each page section.
Updated the method for retrieving all groups in the PdsGroupTable class to use the built-in list constructor for improved clarity and performance. This change enhances code readability while maintaining functionality.
…ions and hidden parameters

Modified the tables_from_pdsfiles method in the PdsGroupTable class to set default values for exclusions and hidden parameters to None, ensuring they are initialized as empty sets if not provided. This change improves the method's robustness and usability.
@rfrenchseti

Copy link
Copy Markdown
Collaborator

Critique of PR #14 ("Clean up viewmaster") vs. the issues it claims to fix

Date: 2026-07-17
Branch: clean_up_viewmaster (HEAD a0d05e2)
PR: #14 — claims "Fixes #3. Fixes #9. Fixes #12."

Methodology: Read the PR description and all three issues, reviewed the full diff
against main, and exercised the app both ways against a real holdings tree
(/data/pdsdata/holdings): the README's local-run command and the WSGI entry path
(create_app() alone via a Flask test client).


Bottom line

The local developer flow genuinely works now — verified that
python -m viewmaster.viewmaster with only PDS3_HOLDINGS_DIR set serves pages,
icons, and deep directory views. But the PR says "Fixes #3. Fixes #9. Fixes #12,"
and merging it would auto-close all three issues while significant bullets in #9
and #12 remain unmet — and it introduces one outright regression: the WSGI
deployment path is broken.


The big one: WSGI is broken (regression vs. #9)

viewmaster.wsgi is now just:

from viewmaster.viewmaster import create_app
application = create_app()

But create_app() never calls initialize_caches() (which runs
Pds3File.preload) or pdsviewable.load_icons() — those moved into the
if __name__ == "__main__" block only (viewmaster/viewmaster.py:2584-2590).
On main, the preload ran at module import time, so WSGI worked.

Verified the failure directly: instantiating the app via create_app() alone and
requesting /volumes returns 404 on every request with
IOError: from_path is not supported without a preload.

Issue #9 explicitly requires: "For a server environment, it needs to be able to be
run using WSGI." The fix is simple — call initialize_caches() and load_icons()
from create_app()/init_once() — but as written, deploying this branch under
Apache serves nothing.


Issue #3 (holdings path with spaces; Python version in wsgi_init.py) — mostly fixed

  • Spaces in holdings path: fixed by design. The path now comes straight from
    the PDS3_HOLDINGS_DIR env var (viewmaster/viewmaster.py:258) instead of
    being word-split out of an Apache conf line. No parsing, no space problem.
  • Python-version bug: fixed by deletion, but the responsibility vanished.
    wsgi_init.py (which computed the venv's site-packages path and got
    two-digit minor versions wrong) was deleted. The new two-line .wsgi files do
    no venv setup at all, and nothing anywhere documents how mod_wsgi is supposed
    to find the venv (e.g., WSGIDaemonProcess python-home=...). The bug is gone
    because the mechanism is gone — but a deployer now has zero guidance, which
    feeds into the Write overall documentation #12 gap below.

Issue #9 (easier for developers) — partially done; PR itself admits it

Genuinely delivered (verified against main)

  • The HTTPD_CUSTOMIZATION Apache-conf parsing is gone.
  • The hostname sniffing (VIEWMASTER_FOR_MARK = 'mark' in socket.gethostname()...)
    is gone.
  • create_holdings_symlinks() is gone — symlink creation is fully removed.
  • requirements.txt works: the repo venv built from it boots the app.
  • Clone → venv → env var → one command → working local server: verified
    end-to-end.

Not delivered or contradicted

  • Symlink lookup still exists at runtime. Issue Clean up Viewmaster so it's easier to use by developers #9 says "no symlinks should
    be looked for or created at runtime, period," but viewmaster/link.py:151
    still globs DOCUMENT_ROOT_ + '/holdings*/' — the document-root symlink
    pattern, alive and load-bearing in the Link service.
  • No package install, no viewmaster CLI, no command-line args for holdings.
    The PR lists pyproject.toml as "pending," yet claims to fix the issue that
    requires it. (The untracked local pyproject.toml on this machine is a copied
    rms-pdsfile file — wrong name, wrong scripts — and isn't in the PR anyway.
    examples/apache2/viewmaster_local.conf is likewise untracked.)
  • Hard-coded environment persists. viewmaster_config.py still hard-codes
    /var/www/, /Library/WebServer/, memcached socket paths,
    https://pds-rings.seti.org/ (also hard-coded in the /feedback route), and
    Mark-era EXTRA_LOCAL_IP_ADDRESS_A_B_C = '10.1.10.'. Only one thing is
    env-var-configurable. The README even instructs developers to
    sudo mkdir /var/www && chown just so logs can be written — the opposite of
    "easier configuration."
  • The hostname heuristic was replaced with an argv heuristic.
    VIEWMASTER_TESTING = sys.argv[0].endswith('viewmaster.py') silently gives
    production config to anyone using flask run, gunicorn, pytest, or
    python -c. Same class of magic issue Clean up Viewmaster so it's easier to use by developers #9 objected to.
  • Config bug, verified live: on Linux, the platform block unconditionally
    overrides the testing-mode memcache ports (the Mac branch respects
    VIEWMASTER_TESTING; the Linux branch doesn't). Every local Linux start burns
    ~3 seconds failing to reach /var/run/memcached/memcached.socket before
    falling back, and warns about unwritable /var/www/logs/webapps/ logs.
  • PDS4 unsupported — the env var is PDS3-only, with an # XXX PDS4 comment
    admitting it; Clean up Viewmaster so it's easier to use by developers #9 says "PDS3/PDS4 holdings."
  • Smaller things:
    • app.run(host='0.0.0.0', debug=True) exposes the Werkzeug debugger (an RCE
      vector) to the whole network, and the debug reloader runs the entire
      multi-second preload twice on every start (both verified in the log).
      Host/port are not configurable.
    • Leftover debug print() in initialize_caches
      (viewmaster/viewmaster.py:455), which prints a double-slash URL.
    • sys.exit(1) inside the create_app path (get_holdings_path), which is
      hostile under WSGI.
    • requirements.txt mixes doc/test tools (sphinx, sphinx-rtd-theme,
      myst-parser, pytest) into runtime deps, and pylibmc — which needs
      libmemcached headers to build — is a hard import even when caching is off:
      an install barrier for a "just clone and run" flow. Nothing is pinned.
    • Hard-coded Flask secret key ("Cassini Grand Finale!").

Issue #12 (documentation) — good where it exists, with one explicit gap

  • The rendering-architecture and PdsFile-rules documentation in the README is
    genuinely useful — screenshots, template mapping, rules-per-section tables.
    Google-style docstrings across all modules plus Sphinx/ReadTheDocs setup
    satisfy the "overall structure of the code and how it works" bullet well.
  • Install/env-setup/run-locally instructions are accurate — followed them
    successfully.
  • "How to run with an Apache webserver" is entirely absent. Zero mentions of
    Apache, WSGI, or any production server in the README or any .rst file — an
    explicit bullet of Write overall documentation #12, and doubly important since wsgi_init.py's venv logic
    was deleted (and, per above, the WSGI path doesn't currently work anyway).
  • The README's three badges (PyPI, build status, codecov) all point at things
    that don't exist — no PyPI package, no GitHub Actions workflows, no coverage.
    They'll render as broken/misleading from day one.
  • Stale docs: the viewmaster_config.py docstring and comment blocks in
    viewmaster.py/link.py still document MAKE_SYMLINKS and WEBSITE_ROOT_,
    which are now dead variables (MAKE_SYMLINKS = True is still set in deployed
    mode but nothing reads it).
  • The README "Documentation" link points at the branch-specific ReadTheDocs
    build (.../en/clean_up_viewmaster/); there's a comment to update it after
    merge — easy to forget.

Code-quality notes on the refactor itself

The pdsgroup.py/pdsgrouptable.py/pdsiterator.py changes are low-risk:
docstrings, mutable-default fixes (pdsfiles=[]None), proper relative
imports plus __init__.py making it a real package (which is what enables
python -m). The one behavioral change — PdsGroup.__init__ now honors its
previously-ignored anchor parameter — is safe; no caller passes it.

One sloppy spot: validate_holdings_paths logs "Directory is missing, ignored"
for missing shelves/volinfo dirs but the continue only skips the inner
loop iteration — the path is accepted anyway (the test run proved it: both
warnings fired and the app ran fine). The message lies, and logger.fatal on a
nonexistent path doesn't actually stop anything either. It also silently
requires the holdings directory to be literally named holdings (after
realpath resolution), which the README never mentions.


Recommendation

Before merge:

  1. Fix the WSGI regression by moving initialize_caches()/load_icons()
    into create_app().
  2. Change the PR description's "Fixes Clean up Viewmaster so it's easier to use by developers #9. Fixes Write overall documentation #12." to "Progresses" (or
    split the remaining work into follow-up issues), since packaging/CLI, Apache
    docs, and the config hard-coding are explicitly still open.
  3. Cheap wins worth folding in now: delete the dead
    MAKE_SYMLINKS/WEBSITE_ROOT_ config and stale comment blocks; remove the
    leftover print; make the Linux testing branch respect VIEWMASTER_TESTING
    for memcache; turn off debug=True (or at least use_reloader=False); drop
    or comment out the broken badges.

Issue #3 is the only one of the three that's honestly closeable as-is — and even
it deserves one sentence somewhere telling deployers how the venv is now supposed
to reach mod_wsgi.

Updated the init_once function to ensure it is idempotent by introducing an _INITIALIZED flag. Modified the get_holdings_path function to raise an exception instead of exiting the program on failure. Enhanced the documentation for both functions to clarify their roles and updated the create_app function's docstring to reflect the new initialization steps. These changes improve the robustness and clarity of the application startup process.
Updated README.md to include placeholders for badges and clarified server binding options. Refactored viewmaster.py to allow dynamic host and port configuration for the local development server, improving flexibility. Removed outdated comments and configuration variables from link.py and viewmaster_config.py to streamline the codebase.
…it tests for holdings path validation

Enhanced the README.md to clarify the requirements for the `PDS3_HOLDINGS_DIR` environment variable, specifying the necessary directory structure. Introduced a new test file `test_validate_holdings_paths.py` to implement unit tests for the `validate_holdings_paths` function, ensuring proper validation of holdings directory paths and their required sibling directories.
Updated README.md to clarify the setup of required environment variables, including `VIEWMASTER_TESTING` and `VIEWMASTER_SECRET_KEY`. Added a new test file `test_config_testing_mode.py` to validate the behavior of the application in testing mode, ensuring proper handling of the secret key and memcache settings based on the environment. These changes improve documentation and testing coverage for the application configuration.
Introduced a new test file `test_link.py` to validate the behavior of the Link service regarding file resolution through the `PDS3_HOLDINGS_DIR`. The tests cover scenarios for directory redirects, file existence checks, and handling of missing files. Updated the link function to ensure files are redirected to `WEBSITE_HTTP_HOME/holdings/...` only if they exist in the specified holdings directory, improving error handling and clarity in the redirection logic.
… add deployment guide

Updated README.md to include detailed instructions for deploying the application with Apache and mod_wsgi, covering environment variables, WSGI entry points, and a minimal vhost configuration. Added a new deployment guide in docs/deployment.rst for comprehensive setup instructions. Introduced an example Apache configuration file in examples/apache2/viewmaster.conf to assist users in configuring their server environment.
Introduced support for PDS4 holdings by adding the `PDS4_HOLDINGS_DIR` environment variable and related validation functions in `viewmaster.py`. Updated the configuration to allow for dynamic log directory management in testing mode, ensuring logs are stored in user-writable locations. Enhanced the README.md to document the new environment variable and its usage, and added unit tests to validate the PDS4 holdings path functionality and its integration with existing configurations. This improves the application's flexibility and usability for users managing PDS4 data.
Simplified the validation logic in `viewmaster.py` to focus solely on the `holdings` directory, removing checks for sibling `shelves` and `volinfo` directories. Updated the README.md to reflect these changes, clarifying the requirements for the `PDS3_HOLDINGS_DIR` environment variable. Additionally, refactored unit tests in `test_validate_holdings_paths.py` to align with the new validation logic, ensuring accurate testing of holdings directory paths.
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.

Viewmaster get holdings path and python version issues

2 participants