Skip to content

refactor: simplify and consolidate helper.py and cli.py#183

Merged
ryansurf merged 2 commits into
ryansurf:mainfrom
raza-khan0108:refactor/simplify-helper-and-cli
Feb 27, 2026
Merged

refactor: simplify and consolidate helper.py and cli.py#183
ryansurf merged 2 commits into
ryansurf:mainfrom
raza-khan0108:refactor/simplify-helper-and-cli

Conversation

@raza-khan0108
Copy link
Copy Markdown
Contributor

@raza-khan0108 raza-khan0108 commented Feb 25, 2026

refactor: simplify and consolidate helper.py and cli.py

Cleans up duplicated and verbose patterns across both files, reducing overall line count by ~158 lines with no behaviour changes.

helper.py

  • Extracted a shared _extract_arg() private helper — extract_decimal, get_forecast_days, and get_color all shared the same "scan args for key=value, parse, return default" loop; replaced with thin wrappers over the shared helper
  • Merged surf_summary into print_gpt — it had no callers outside of print_gpt
  • Simplified round_decimal to a list comprehension
  • Replaced range(len(...)) with enumerate in forecast_to_json
  • Fixed long-standing typo: seperate_argsseparate_args; old name kept as an alias for backwards compatibility

cli.py

  • Unpacked set_location return value directly instead of manually indexing [0], [1], [2]
  • Computed json_output once before the if/else branch instead of duplicating the call in both branches

General:

  • Have you followed the guidelines in our Contributing document?
  • Have you checked to ensure there aren't other open Pull Requests for the same update/change?

Code:

  1. Does your submission pass tests?
  2. Have you run the linter/formatter on your code locally before submission?
  3. Have you updated the documentation/README to reflect your changes, as applicable?
  4. Have you added an explanation of what your changes do?
  5. Have you written new tests for your changes, as applicable?

Summary by Sourcery

Refactor helper and CLI argument handling to reduce duplication and simplify control flow without changing observable behaviour.

Enhancements:

  • Introduce a shared internal helper for parsing key=value CLI arguments and refactor decimal, forecast-days, and color extraction to use it.
  • Tighten and clarify helper utilities, including rounding, forecast JSON conversion, location unpacking, and GPT summary generation.
  • Simplify CLI run flow by using the new helpers, unpacking locations directly, and computing JSON output once for reuse.
  • Clean up comments, docstrings, and naming (including fixing the separate_args typo while keeping a backwards-compatible alias).

- Extract shared _extract_arg() helper; simplify extract_decimal,
  get_forecast_days, and get_color to thin wrappers
- Merge surf_summary into print_gpt (was only called there)
- Simplify round_decimal to a list comprehension
- Use enumerate in forecast_to_json instead of range(len(...))
- Fix typo: seperate_args -> separate_args (alias kept for compat)
- cli.py: unpack set_location tuple directly
- cli.py: compute json_output once before if/else branch
Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

👋 Hi! Thanks for submitting your first pull request!
• We appreciate your effort to improve this project.
• If you're enjoying your experience, please consider giving us a star ⭐
• It helps us grow and motivates us to keep improving! 🚀

@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai Bot commented Feb 25, 2026

Reviewer's Guide

Refactors shared CLI/helper logic by introducing a generic argument extractor, simplifying rounding and forecast JSON construction, merging the GPT surf summary into a single function, and cleaning up cli.run to unpack locations and reuse a single JSON computation, all while preserving behavior and adding a backwards-compatible alias for the old argument-splitting helper.

Sequence diagram for updated cli.run execution and JSON handling

sequenceDiagram
    actor User
    participant CLI as cli_run
    participant Helper as helper
    participant API as api
    participant DB as db_handler

    User->>CLI: run(lat?, long?, args?)
    alt args is None
        CLI->>Helper: separate_args(sys.argv)
        Helper-->>CLI: args
    else args provided
        CLI->>Helper: separate_args(args)
        Helper-->>CLI: args
    end

    CLI->>API: seperate_args_and_get_location(args)
    API-->>CLI: location

    CLI->>Helper: set_location(location)
    Helper-->>CLI: city, loc_lat, loc_long

    alt lat == 0 and long == 0
        CLI->>CLI: lat, long = loc_lat, loc_long
    end

    CLI->>Helper: arguments_dictionary(lat, long, city, args)
    Helper-->>CLI: arguments

    CLI->>API: gather_data(lat, long, arguments)
    API-->>CLI: ocean_data_dict

    CLI->>Helper: json_output(ocean_data_dict, print_output=False)
    Helper-->>CLI: json_out

    alt arguments[json_output] == 0
        CLI->>Helper: print_outputs(ocean_data_dict, arguments, gpt_prompt, gpt_info)
        Helper-->>CLI: response
        alt db_uri is set
            CLI->>DB: insert_report(json_out)
        end
        CLI-->>User: ocean_data_dict, response
    else JSON output requested
        alt db_uri is set
            CLI->>DB: insert_report(json_out)
        end
        CLI-->>User: json_out
    end
Loading

Class diagram for updated helper.py argument and GPT utilities

classDiagram
    class HelperModule {
        +arguments_dictionary(lat, long, city, args)
        +set_output_values(args, arguments_dictionary)
        +separate_args(args)
        +seperate_args(args)
        +_extract_arg(args, keys, default, cast)
        +extract_decimal(args)
        +get_forecast_days(args)
        +get_color(args)
        +print_location(city, show_city)
        +print_ocean_data(arguments_dict, ocean_data_dict)
        +print_forecast(ocean, forecast)
        +round_decimal(round_list, decimal)
        +json_output(data_dict, print_output)
        +print_outputs(ocean_data_dict, arguments, gpt_prompt, gpt_info)
        +set_location(location)
        +forecast_to_json(forecast_data, decimal)
        +print_gpt(surf_data, gpt_prompt, gpt_info)
    }

    class ArgExtractionFlow {
        +arguments_dictionary(lat, long, city, args)
        +extract_decimal(args)
        +get_forecast_days(args)
        +get_color(args)
        +_extract_arg(args, keys, default, cast)
    }

    class GPTFlow {
        +print_outputs(ocean_data_dict, arguments, gpt_prompt, gpt_info)
        +print_gpt(surf_data, gpt_prompt, gpt_info)
    }

    HelperModule <.. ArgExtractionFlow
    HelperModule <.. GPTFlow

    ArgExtractionFlow --> HelperModule : uses
    GPTFlow --> HelperModule : uses

    ArgExtractionFlow ..> _extract_arg : internal_shared_helper
    HelperModule ..> _extract_arg : internal_use
    HelperModule ..> extract_decimal : wraps__extract_arg
    HelperModule ..> get_forecast_days : wraps__extract_arg
    HelperModule ..> get_color : wraps__extract_arg

    GPTFlow ..> print_gpt : builds_summary_and_selects_model
    HelperModule ..> print_outputs : orchestrates_printing_and_forecast
Loading

File-Level Changes

Change Details Files
Introduce a shared argument-extraction helper and reimplement specific CLI argument parsers on top of it.
  • Add a private _extract_arg that scans args for key=value tokens, applies a cast, and returns a default on error or absence.
  • Reimplement extract_decimal using _extract_arg with integer casting and default of 1, with defensive handling of invalid values.
  • Reimplement get_forecast_days using _extract_arg, enforcing a [0,7] bound and improving the error message wording.
  • Reimplement get_color as a thin wrapper over _extract_arg defaulting to 'blue'.
src/helper.py
Tidy argument handling utilities and printing helpers for clarity and maintainability.
  • Rename seperate_args to separate_args, returning directly instead of using a temporary list, and keep the old name as an alias for backwards compatibility.
  • Simplify set_output_values docstring and remove redundant comments while keeping the mapping-based flag handling intact.
  • Make print_location, print_ocean_data, print_forecast, json_output, and print_outputs docstrings and comments more concise without altering behavior.
  • Inline the forecast_days lookup in print_forecast instead of assigning an intermediate variable.
src/helper.py
src/cli.py
Simplify data transformation helpers for rounding and forecast JSON construction.
  • Refactor round_decimal into a list comprehension returning a new rounded list.
  • Change forecast_to_json to iterate with enumerate over forecast_data['date'] instead of range(len(...)), using the loop index to access parallel arrays and slightly compacting the JSON field construction.
src/helper.py
Consolidate GPT surf summary generation into print_gpt and simplify its logic.
  • Remove the standalone surf_summary helper and build the summary string inline in print_gpt using f-string composition.
  • Unpack gpt_info into api_key and gpt_model in a single assignment and rename the minimum key length constant to MIN_KEY_LEN for clarity.
  • Return the GPT responses directly from each branch instead of assigning to a temporary variable first.
src/helper.py
Streamline cli.run argument parsing, location unpacking, and JSON handling.
  • Switch run to use helper.separate_args instead of the misspelled helper.seperate_args for both default sys.argv and explicit args paths.
  • Unpack city, lat, long directly from helper.set_location(location) instead of indexing into a tuple, and only override lat/long from the location when the function arguments are at their default (0,0).
  • Precompute json_out = helper.json_output(ocean_data_dict, print_output=False) once and reuse it in both the non-JSON and JSON branches, as well as for DB insertion, eliminating duplicate calls and variable names.
  • Slightly tighten comments in cli.run to describe behavior without restating obvious control flow.
src/cli.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 2 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="src/helper.py" line_range="165-174" />
<code_context>
+    return value
+
+
+def get_forecast_days(args):
+    """
+    Extract forecast day count from CLI args. Defaults to 0. Max is 7.
+    """
+    MAX_VALUE = 7
+    value = _extract_arg(args, ["forecast", "fc"], default=0, cast=int)
+    if value < 0 or value > MAX_VALUE:
+        print("Must choose a non-negative number <= 7 in forecast!")
+        return 0
</code_context>
<issue_to_address>
**suggestion:** Consider making the forecast-days error handling and message use the configured default and max consistently.

Invalid values currently log a message and reset to `0`, even though you already define `MAX_VALUE` and pass a default of `0` to `_extract_arg`. It would be clearer to interpolate `MAX_VALUE` in the message (e.g. `<= {MAX_VALUE}`) and return the same default value used by `_extract_arg` instead of a hardcoded `0`, so behavior stays consistent if the default or max change later.

```suggestion
def get_forecast_days(args):
    """
    Extract forecast day count from CLI args. Defaults to 0. Max is 7.
    """
    DEFAULT_VALUE = 0
    MAX_VALUE = 7
    value = _extract_arg(args, ["forecast", "fc"], default=DEFAULT_VALUE, cast=int)
    if value < 0 or value > MAX_VALUE:
        print(
            f"Must choose a non-negative number <= {MAX_VALUE} in forecast! "
            f"Using default ({DEFAULT_VALUE})."
        )
        return DEFAULT_VALUE
    return value
```
</issue_to_address>

### Comment 2
<location path="src/helper.py" line_range="375-377" />
<code_context>
+        f"swell is {surf_data['Swell Direction']} degrees and the swell "
+        f"period is {surf_data['Period']} seconds."
+    )
+    api_key, gpt_model = gpt_info
+    MIN_KEY_LEN = 5
+    if not api_key or len(api_key) < MIN_KEY_LEN:
+        return gpt.simple_gpt(summary, gpt_prompt)
+    return gpt.openai_gpt(summary, gpt_prompt, api_key, gpt_model)
</code_context>
<issue_to_address>
**suggestion (bug_risk):** API key validation in `print_gpt` may treat whitespace-only keys as valid.

The condition `if not api_key or len(api_key) < MIN_KEY_LEN` considers whitespace-only strings (length ≥ 5) as valid and sends them to `openai_gpt`, likely causing a less controlled failure. Consider normalizing first, e.g. `normalized = (api_key or '').strip()`, then checking `if not normalized or len(normalized) < MIN_KEY_LEN:` and passing `normalized` onward.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/helper.py
Comment on lines +165 to +174
def get_forecast_days(args):
"""
Extract forecast day count from CLI args. Defaults to 0. Max is 7.
"""
MAX_VALUE = 7
value = _extract_arg(args, ["forecast", "fc"], default=0, cast=int)
if value < 0 or value > MAX_VALUE:
print("Must choose a non-negative number <= 7 in forecast!")
return 0
return value
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion: Consider making the forecast-days error handling and message use the configured default and max consistently.

Invalid values currently log a message and reset to 0, even though you already define MAX_VALUE and pass a default of 0 to _extract_arg. It would be clearer to interpolate MAX_VALUE in the message (e.g. <= {MAX_VALUE}) and return the same default value used by _extract_arg instead of a hardcoded 0, so behavior stays consistent if the default or max change later.

Suggested change
def get_forecast_days(args):
"""
Extract forecast day count from CLI args. Defaults to 0. Max is 7.
"""
MAX_VALUE = 7
value = _extract_arg(args, ["forecast", "fc"], default=0, cast=int)
if value < 0 or value > MAX_VALUE:
print("Must choose a non-negative number <= 7 in forecast!")
return 0
return value
def get_forecast_days(args):
"""
Extract forecast day count from CLI args. Defaults to 0. Max is 7.
"""
DEFAULT_VALUE = 0
MAX_VALUE = 7
value = _extract_arg(args, ["forecast", "fc"], default=DEFAULT_VALUE, cast=int)
if value < 0 or value > MAX_VALUE:
print(
f"Must choose a non-negative number <= {MAX_VALUE} in forecast! "
f"Using default ({DEFAULT_VALUE})."
)
return DEFAULT_VALUE
return value

Comment thread src/helper.py
Comment on lines +375 to +377
api_key, gpt_model = gpt_info
MIN_KEY_LEN = 5
if not api_key or len(api_key) < MIN_KEY_LEN:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion (bug_risk): API key validation in print_gpt may treat whitespace-only keys as valid.

The condition if not api_key or len(api_key) < MIN_KEY_LEN considers whitespace-only strings (length ≥ 5) as valid and sends them to openai_gpt, likely causing a less controlled failure. Consider normalizing first, e.g. normalized = (api_key or '').strip(), then checking if not normalized or len(normalized) < MIN_KEY_LEN: and passing normalized onward.

@ryansurf
Copy link
Copy Markdown
Owner

Hey @raza-khan0108, thanks for the contribution! The code definitely looks cleaner with these changes.

It lgtm, but can you make sure you run the linter and check on the failing test?

@raza-khan0108
Copy link
Copy Markdown
Contributor Author

Sure, i will take a look at it and make the necessary changes

- Wrap long lines in helper.py and cli.py to stay within 79 char limit
- Use ruff auto-format on both files to ensure formatter passes
- Restore extract_decimal's exact error message to fix test_invalid_input:
  'Invalid value for decimal. Please provide an integer.'
@raza-khan0108
Copy link
Copy Markdown
Contributor Author

Changes Made

src/helper.py

  • Extracted _extract_arg() helperget_forecast_days and get_color now use a shared private helper instead of duplicating the same arg-scanning loop
  • extract_decimal — kept standalone to preserve its exact error message ("Invalid value for decimal. Please provide an integer.") required by the test suite
  • Merged surf_summary into print_gptsurf_summary was only ever called by print_gpt, so it didn't need to be its own public function
  • Simplified round_decimal — replaced manual loop with a list comprehension
  • forecast_to_json — replaced range(len(...)) with enumerate
  • Fixed typo seperate_argsseparate_args; old name kept as an alias for backwards compatibility

src/cli.py

  • Direct tuple unpacking of set_location return value instead of manually indexing [0], [1], [2]
  • Deduplicated json_output call — computed once before the if/else branch (was duplicated in both branches)
  • Updated seperate_argsseparate_args call

CI Fixes

  • Wrapped all lines exceeding the 79-char ruff limit in both files
  • Restored extract_decimal's exact error message to fix test_invalid_input

@ryansurf
Copy link
Copy Markdown
Owner

@raza-khan0108 this looks great, thanks for the contribution

@ryansurf ryansurf merged commit 28b5a29 into ryansurf:main Feb 27, 2026
3 checks passed
@ryansurf
Copy link
Copy Markdown
Owner

@all-contributors please add @raza-khan0108 for refactor

@allcontributors
Copy link
Copy Markdown
Contributor

@ryansurf

I couldn't determine any contributions to add, did you specify any contributions?
Please make sure to use valid contribution names.

@ryansurf
Copy link
Copy Markdown
Owner

@all-contributors please add @raza-khan0108 for refactoring code

@allcontributors
Copy link
Copy Markdown
Contributor

@ryansurf

I've put up a pull request to add @raza-khan0108! 🎉

@codecov
Copy link
Copy Markdown

codecov Bot commented Feb 27, 2026

Codecov Report

❌ Patch coverage is 64.44444% with 16 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/cli.py 0.00% 8 Missing ⚠️
src/helper.py 78.37% 8 Missing ⚠️
Files with missing lines Coverage Δ
src/cli.py 0.00% <0.00%> (ø)
src/helper.py 59.09% <78.37%> (+0.03%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants