Skip to content

Add analyze_text() and analyze_lines() API with stdin support#39

Merged
calebevans merged 1 commit into
mainfrom
feat/text-api-and-stdin
May 23, 2026
Merged

Add analyze_text() and analyze_lines() API with stdin support#39
calebevans merged 1 commit into
mainfrom
feat/text-api-and-stdin

Conversation

@calebevans

@calebevans calebevans commented May 23, 2026

Copy link
Copy Markdown
Owner

Summary

  • Add analyze_text(text: str) -> str and analyze_text_detailed(text: str) -> AnalysisResult methods for analyzing raw log text without writing to a temp file
  • Add analyze_lines(lines: Sequence[tuple[int, str]]) -> AnalysisResult for users who already have structured line data
  • Refactor pipeline to share _analyze_lines() internal method between all entry points, eliminating duplication
  • Support - as filename in CLI to read from stdin (e.g., kubectl logs pod | cordon -)
  • Add analyze_stdin() CLI function for stdin handling

Summary by CodeRabbit

  • New Features

    • Use - as the logfile argument to analyze piped stdin; output can go to stdout or be written to a file (respecting overwrite protection).
    • Added text-based analysis entrypoints for analyzing raw text or pre-numbered line sequences without file I/O.
  • Refactor

    • Consolidated result rendering and detailed-statistics behavior for more consistent output.
  • Tests

    • Added CLI and analyzer tests for stdin and text/line analysis.

Review Change Stack

@calebevans calebevans self-assigned this May 23, 2026
@calebevans calebevans added the enhancement New feature or request label May 23, 2026
@coderabbitai

coderabbitai Bot commented May 23, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@calebevans, we couldn't start this review because you've used your available PR reviews for now.

Your plan currently allows 2 reviews/hour. Refill in 22 minutes and 17 seconds.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more review capacity refills, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than trial, open-source, and free plans. In all cases, review capacity refills continuously over time.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5b0face5-b0f7-430e-9315-b129fceeb4b5

📥 Commits

Reviewing files that changed from the base of the PR and between c8fb011 and 56436ff.

📒 Files selected for processing (4)
  • src/cordon/cli.py
  • src/cordon/pipeline.py
  • tests/test_cli.py
  • tests/test_pipeline_unit.py
📝 Walkthrough

Walkthrough

This PR adds support for analyzing logs from standard input by introducing text-based analysis methods to SemanticLogAnalyzer, integrating them into the CLI via a new analyze_stdin handler, and validating both pathways with unit tests and argument-parsing checks.

Changes

Stdin Log Analysis Support

Layer / File(s) Summary
Text analysis APIs and pipeline refactor
src/cordon/pipeline.py
Refactors analyze_file_detailed to delegate to a new internal _analyze_lines method; introduces three new public methods (analyze_text, analyze_text_detailed, analyze_lines) that accept text or pre-numbered line tuples; updates merging to store merged_blocks_count immediately and uses it in AnalysisResult construction.
CLI stdin handler and routing
src/cordon/cli.py
Adds analyze_stdin helper that reads stdin, routes to either analyze_text_detailed or analyze_text based on the detailed flag, handles output via the new _display_results helper (writing to file or stdout), updates help text to document '-' for stdin, and routes stdin paths through analyze_stdin in the main loop.
Unit and integration tests
tests/test_cli.py, tests/test_pipeline_unit.py
Adds CLI argument-parsing test for "-" as a logfile argument; introduces TestAnalyzeText suite with four tests covering analyze_text_detailed (non-empty and empty cases), analyze_text output, and analyze_lines with pre-numbered tuples.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A stdin stream now flows so free,
Through text-based pipes and APIs,
The analyzer reads from thee—
No files needed, just apply,
Our dashingly refactored pipeline ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: adding new text/lines analysis API methods and stdin support, which aligns with the core objectives and file modifications.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/text-api-and-stdin

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

❤️ Share

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

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

🧹 Nitpick comments (3)
src/cordon/pipeline.py (1)

121-130: ⚡ Quick win

Harden analyze_lines() input to prevent silent line collisions.

At Line 121, this public API accepts arbitrary tuples, but downstream formatting uses dict(lines), so duplicate line numbers overwrite earlier content silently. Add a lightweight validation for strictly increasing, unique line numbers before forwarding.

Proposed change
 def analyze_lines(self, lines: Sequence[tuple[int, str]]) -> AnalysisResult:
@@
-        return self._analyze_lines(list(lines))
+        lines_list = list(lines)
+        line_numbers = [line_number for line_number, _ in lines_list]
+        if line_numbers != sorted(line_numbers):
+            raise ValueError("lines must be sorted by ascending line number")
+        if len(set(line_numbers)) != len(line_numbers):
+            raise ValueError("lines must not contain duplicate line numbers")
+        return self._analyze_lines(lines_list)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cordon/pipeline.py` around lines 121 - 130, The public method
analyze_lines currently accepts arbitrary (line_number, line_content) tuples and
forwards them to _analyze_lines after converting to list, which can silently
allow duplicate or out-of-order line numbers (downstream uses dict(lines)); add
a lightweight validation at the start of analyze_lines: verify each item is a
tuple of (int, str), ensure line_number values are strictly increasing and
unique (previous_line_num < current_line_num) and raise a ValueError (or
TypeError for wrong types) with a clear message if the check fails, then call
self._analyze_lines(list(lines)) as before; reference analyze_lines and
_analyze_lines to locate where to add this check.
src/cordon/cli.py (1)

271-327: ⚡ Quick win

Factor out shared reporting/output flow used by analyze_file() and analyze_stdin().

This block mirrors analyze_file() almost line-for-line. Extracting the common “run analysis + print detailed stats + emit/write content” path into one helper will reduce drift risk and simplify future changes.

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

In `@src/cordon/cli.py` around lines 271 - 327, The analyze_stdin function
duplicates the reporting/output flow found in analyze_file; create a shared
helper (e.g., _run_analysis_and_output or similar) and move the common
logic—running detailed vs. simple analysis using analyzer.analyze_text_detailed
/ analyzer.analyze_text, the try/except around analysis, printing the header and
detailed stats (total_lines, total_windows, significant_windows, merged_blocks,
processing_time, score_distribution), and the final emission via _write_output
or stdout—into that helper; then have analyze_stdin and analyze_file call the
new helper with appropriate arguments (analyzer, text, detailed, output_path,
force) to eliminate the near-identical code paths and keep only small
I/O-specific bits in each function.
tests/test_cli.py (1)

87-92: ⚡ Quick win

Add behavioral CLI tests for stdin execution path (not just argument parsing).

This test is useful, but the new runtime behavior is in analyze_stdin() and "-" routing in _main_impl(). Please add tests that mock stdin/analyzer and assert the correct analyzer method is called for detailed vs non-detailed modes.

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

In `@tests/test_cli.py` around lines 87 - 92, Add runtime tests that exercise
analyze_stdin() and the "-" routing in _main_impl(): set sys.argv to include "-"
and replace sys.stdin with an io.StringIO containing test content, then
monkeypatch the Analyzer instance methods used by analyze_stdin() (e.g.,
Analyzer.analyze and Analyzer.analyze_detailed or whatever concrete methods
_main_impl() calls) to simple spies/mocks; call _main_impl() (or the public CLI
entry that triggers it) and assert the correct analyzer method was invoked for
both non-detailed and detailed flags. Ensure you add two cases: one with the
detailed flag set (expects the detailed analyze method called) and one without
(expects the simple analyze method), and restore monkeypatched stdin/argv after
each test.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/cordon/cli.py`:
- Around line 271-327: The analyze_stdin function duplicates the
reporting/output flow found in analyze_file; create a shared helper (e.g.,
_run_analysis_and_output or similar) and move the common logic—running detailed
vs. simple analysis using analyzer.analyze_text_detailed /
analyzer.analyze_text, the try/except around analysis, printing the header and
detailed stats (total_lines, total_windows, significant_windows, merged_blocks,
processing_time, score_distribution), and the final emission via _write_output
or stdout—into that helper; then have analyze_stdin and analyze_file call the
new helper with appropriate arguments (analyzer, text, detailed, output_path,
force) to eliminate the near-identical code paths and keep only small
I/O-specific bits in each function.

In `@src/cordon/pipeline.py`:
- Around line 121-130: The public method analyze_lines currently accepts
arbitrary (line_number, line_content) tuples and forwards them to _analyze_lines
after converting to list, which can silently allow duplicate or out-of-order
line numbers (downstream uses dict(lines)); add a lightweight validation at the
start of analyze_lines: verify each item is a tuple of (int, str), ensure
line_number values are strictly increasing and unique (previous_line_num <
current_line_num) and raise a ValueError (or TypeError for wrong types) with a
clear message if the check fails, then call self._analyze_lines(list(lines)) as
before; reference analyze_lines and _analyze_lines to locate where to add this
check.

In `@tests/test_cli.py`:
- Around line 87-92: Add runtime tests that exercise analyze_stdin() and the "-"
routing in _main_impl(): set sys.argv to include "-" and replace sys.stdin with
an io.StringIO containing test content, then monkeypatch the Analyzer instance
methods used by analyze_stdin() (e.g., Analyzer.analyze and
Analyzer.analyze_detailed or whatever concrete methods _main_impl() calls) to
simple spies/mocks; call _main_impl() (or the public CLI entry that triggers it)
and assert the correct analyzer method was invoked for both non-detailed and
detailed flags. Ensure you add two cases: one with the detailed flag set
(expects the detailed analyze method called) and one without (expects the simple
analyze method), and restore monkeypatched stdin/argv after each test.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6a692319-6e4e-40d0-b7fe-9ebe8bcbfffe

📥 Commits

Reviewing files that changed from the base of the PR and between 006d26d and 62a8cbf.

📒 Files selected for processing (4)
  • src/cordon/cli.py
  • src/cordon/pipeline.py
  • tests/test_cli.py
  • tests/test_pipeline_unit.py

@calebevans
calebevans force-pushed the feat/text-api-and-stdin branch from 62a8cbf to c8fb011 Compare May 23, 2026 22:28

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

Caution

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

⚠️ Outside diff range comments (3)
tests/test_cli.py (3)

189-206: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Test mocks wrong method after refactor.

analyze_file now always calls analyze_file_detailed, but this test mocks analyzer.analyze_file.return_value. The test will fail because the actual call to analyze_file_detailed is not mocked.

Proposed fix
     def test_force_overwrite(self, tmp_path: Path) -> None:
         """Test that --force allows overwriting."""
         log_file = tmp_path / "test.log"
         log_file.write_text("line 1\n")
         output_file = tmp_path / "output.xml"
         output_file.write_text("existing content")
 
         analyzer = MagicMock()
-        analyzer.analyze_file.return_value = "<anomalies>new</anomalies>"
+        mock_result = MagicMock()
+        mock_result.output = "<anomalies>new</anomalies>"
+        analyzer.analyze_file_detailed.return_value = mock_result
 
         analyze_file(
             log_file,
             analyzer,
             detailed=False,
             output_path=output_file,
             force=True,
         )
         assert output_file.read_text() == "<anomalies>new</anomalies>"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_cli.py` around lines 189 - 206, The test test_force_overwrite is
mocking analyzer.analyze_file but analyze_file now always calls
analyze_file_detailed; update the test to mock the correct method on the
analyzer (set analyzer.analyze_file_detailed.return_value or appropriate
side_effect) so the call inside analyze_file returns the expected
"<anomalies>new</anomalies>" and the rest of the assertion remains unchanged.

126-139: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Test mocks wrong method after refactor.

analyze_file now calls analyze_file_detailed instead of analyze_file, but this test mocks analyzer.analyze_file.side_effect. The mock won't trigger and the test will fail or behave unexpectedly.

Proposed fix
     def test_analysis_error_non_detailed(
         self, capsys: pytest.CaptureFixture[str], tmp_path: Path
     ) -> None:
         """Test that analysis errors in non-detailed mode are caught."""
         log_file = tmp_path / "test.log"
         log_file.write_text("line 1\nline 2\n")
 
         analyzer = MagicMock()
-        analyzer.analyze_file.side_effect = ValueError("Bad input")
+        analyzer.analyze_file_detailed.side_effect = ValueError("Bad input")
 
         analyze_file(log_file, analyzer, detailed=False)
         captured = capsys.readouterr()
         assert "Error analyzing" in captured.err
         assert "Bad input" in captured.err
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_cli.py` around lines 126 - 139, The test currently sets a side
effect on analyzer.analyze_file but analyze_file() now delegates to
analyzer.analyze_file_detailed; update the mock to set
analyzer.analyze_file_detailed.side_effect = ValueError("Bad input") (or
otherwise mock the method analyze_file_detailed) so the exception is raised when
analyze_file(log_file, analyzer, detailed=False) calls into the refactored code;
ensure references to analyzer.analyze_file in this test are replaced with
analyzer.analyze_file_detailed to match the new implementation.

208-218: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Test mocks wrong method after refactor.

analyze_file now always calls analyze_file_detailed, but this test mocks analyzer.analyze_file.return_value. The test will fail.

Proposed fix
     def test_stdout_output(self, capsys: pytest.CaptureFixture[str], tmp_path: Path) -> None:
         """Test that output is printed to stdout when no output path is given."""
         log_file = tmp_path / "test.log"
         log_file.write_text("line 1\n")
 
         analyzer = MagicMock()
-        analyzer.analyze_file.return_value = "<anomalies>results</anomalies>"
+        mock_result = MagicMock()
+        mock_result.output = "<anomalies>results</anomalies>"
+        analyzer.analyze_file_detailed.return_value = mock_result
 
         analyze_file(log_file, analyzer, detailed=False)
         captured = capsys.readouterr()
         assert "<anomalies>results</anomalies>" in captured.out
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_cli.py` around lines 208 - 218, The test test_stdout_output is
mocking analyzer.analyze_file but analyze_file now always calls
analyzer.analyze_file_detailed; update the test to mock
analyzer.analyze_file_detailed (set analyzer.analyze_file_detailed.return_value
= "<anomalies>results</anomalies>") so analyze_file() receives the expected
result, or adjust the MagicMock to provide both methods, ensuring
analyze_file_detailed is the one returning the expected string when analyze_file
calls it.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@tests/test_cli.py`:
- Around line 189-206: The test test_force_overwrite is mocking
analyzer.analyze_file but analyze_file now always calls analyze_file_detailed;
update the test to mock the correct method on the analyzer (set
analyzer.analyze_file_detailed.return_value or appropriate side_effect) so the
call inside analyze_file returns the expected "<anomalies>new</anomalies>" and
the rest of the assertion remains unchanged.
- Around line 126-139: The test currently sets a side effect on
analyzer.analyze_file but analyze_file() now delegates to
analyzer.analyze_file_detailed; update the mock to set
analyzer.analyze_file_detailed.side_effect = ValueError("Bad input") (or
otherwise mock the method analyze_file_detailed) so the exception is raised when
analyze_file(log_file, analyzer, detailed=False) calls into the refactored code;
ensure references to analyzer.analyze_file in this test are replaced with
analyzer.analyze_file_detailed to match the new implementation.
- Around line 208-218: The test test_stdout_output is mocking
analyzer.analyze_file but analyze_file now always calls
analyzer.analyze_file_detailed; update the test to mock
analyzer.analyze_file_detailed (set analyzer.analyze_file_detailed.return_value
= "<anomalies>results</anomalies>") so analyze_file() receives the expected
result, or adjust the MagicMock to provide both methods, ensuring
analyze_file_detailed is the one returning the expected string when analyze_file
calls it.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3886961b-3bb0-40d0-bbf2-b212bce46aed

📥 Commits

Reviewing files that changed from the base of the PR and between 62a8cbf and c8fb011.

📒 Files selected for processing (4)
  • src/cordon/cli.py
  • src/cordon/pipeline.py
  • tests/test_cli.py
  • tests/test_pipeline_unit.py

- Add analyze_text() and analyze_text_detailed() for analyzing raw
  text without writing to a temp file
- Add analyze_lines() for pre-structured line input
- Refactor pipeline to share _analyze_lines() between all entry points
- Support '-' as filename in CLI to read from stdin
- Add tests for text analysis and stdin support

Co-authored-by: Cursor <cursoragent@cursor.com>
@calebevans
calebevans force-pushed the feat/text-api-and-stdin branch from c8fb011 to 56436ff Compare May 23, 2026 22:31
@sonarqubecloud

Copy link
Copy Markdown

@calebevans
calebevans merged commit 25d0e7a into main May 23, 2026
14 checks passed
@calebevans
calebevans deleted the feat/text-api-and-stdin branch May 23, 2026 22:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant