Skip to content

Bugfix server crash#201

Merged
ryansurf merged 3 commits into
mainfrom
bugfix-server-crash
Jun 2, 2026
Merged

Bugfix server crash#201
ryansurf merged 3 commits into
mainfrom
bugfix-server-crash

Conversation

@ryansurf
Copy link
Copy Markdown
Owner

@ryansurf ryansurf commented Jun 2, 2026

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

Fix server crashes when querying Open-Meteo data by making the HTTP client more resilient and handling empty responses safely.

Bug Fixes:

  • Prevent SQLite cache-related crashes by using a thread-local Open-Meteo client and recreating it on database errors.
  • Avoid crashes when Open-Meteo returns no current data by safely handling missing current values in UV, ocean, and wind/temperature helpers.

Enhancements:

  • Introduce a resilient client wrapper around the Open-Meteo client to centralize error handling for API calls.

Build:

  • Bump package version to 2.5.1 to reflect the bugfix release.

@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai Bot commented Jun 2, 2026

Reviewer's Guide

Makes the Open-Meteo client thread-local and resilient to SQLite cache corruption, and hardens API response handling against missing current data while bumping the package patch version.

Sequence diagram for resilient Open-Meteo weather_api calls

sequenceDiagram
    participant Caller
    participant ResilientClient as _ResilientClient
    participant ThreadLocal as _thread_local
    participant Builder as _build_client
    participant OMClient as openmeteo_requests.Client

    Caller->>ResilientClient: weather_api(url, params)
    ResilientClient->>ThreadLocal: _get_thread_client()
    alt [no client on thread]
        ThreadLocal->>Builder: _build_client()
        Builder-->>ThreadLocal: openmeteo_requests.Client
        ThreadLocal-->>ResilientClient: client
    else [client exists]
        ThreadLocal-->>ResilientClient: client
    end

    ResilientClient->>OMClient: weather_api(url, params)
    alt [sqlite3.DatabaseError]
        ResilientClient->>Builder: _build_client()
        Builder-->>ResilientClient: new_client
        ResilientClient->>OMClient: weather_api(url, params)
        OMClient-->>ResilientClient: response
    else [success]
        OMClient-->>ResilientClient: response
    end
    ResilientClient-->>Caller: response
Loading

File-Level Changes

Change Details Files
Introduce a thread-local, resilient Open-Meteo client that rebuilds its cached session on SQLite database errors to prevent server crashes.
  • Refactor client creation into a _build_client helper that sets up the SQLite-backed cache and retry-enabled session using a shared cache path constant.
  • Add a thread-local storage object and _get_thread_client function to lazily create and store a client per thread.
  • Wrap the Open-Meteo client in a _ResilientClient class whose weather_api method catches sqlite3.DatabaseError, recreates the underlying client, and retries the request.
  • Replace the global openmeteo_client instance with an instance of _ResilientClient.
src/open_meteo.py
Guard API helpers against missing Current() data to avoid attribute access errors and server crashes.
  • In get_uv, check if response.Current() is None and return a simple "No data" sentinel value when no current data is available.
  • In ocean_information, return a default list of zeros when response.Current() is None instead of accessing variables on a null object.
  • In current_wind_temp, return a default list of zeros when response.Current() is None.
src/api.py
Bump package version to reflect the bugfix release.
  • Update the Poetry project version from 2.5.0 to 2.5.1.
pyproject.toml

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

@codecov
Copy link
Copy Markdown

codecov Bot commented Jun 2, 2026

Codecov Report

❌ Patch coverage is 44.44444% with 15 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/open_meteo.py 47.61% 11 Missing ⚠️
src/api.py 33.33% 4 Missing ⚠️
Files with missing lines Coverage Δ
src/api.py 70.85% <33.33%> (-1.04%) ⬇️
src/open_meteo.py 52.00% <47.61%> (-48.00%) ⬇️
🚀 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.

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 3 issues, and left some high level feedback:

  • The new _ResilientClient only wraps weather_api, so if other client methods are used now or in the future they will bypass the retry/rebuild logic; consider either exposing and wrapping the underlying client more generically or documenting that only weather_api is supported.
  • In get_uv, ocean_information, and current_wind_temp, the new current is None branches change the return types (e.g., string vs numeric, lists of zeros vs the usual float values), which may be surprising to callers; consider using a sentinel like None or a typed dataclass/struct so the API remains type-consistent.
  • When catching sqlite3.DatabaseError in _ResilientClient.weather_api, it may be helpful to at least log the error before recreating the client so that underlying cache/database issues are observable rather than silently masked.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The new `_ResilientClient` only wraps `weather_api`, so if other client methods are used now or in the future they will bypass the retry/rebuild logic; consider either exposing and wrapping the underlying client more generically or documenting that only `weather_api` is supported.
- In `get_uv`, `ocean_information`, and `current_wind_temp`, the new `current is None` branches change the return types (e.g., string vs numeric, lists of zeros vs the usual float values), which may be surprising to callers; consider using a sentinel like `None` or a typed dataclass/struct so the API remains type-consistent.
- When catching `sqlite3.DatabaseError` in `_ResilientClient.weather_api`, it may be helpful to at least log the error before recreating the client so that underlying cache/database issues are observable rather than silently masked.

## Individual Comments

### Comment 1
<location path="src/open_meteo.py" line_range="28-24" />
<code_context>
+    return _thread_local.client
+
+
+class _ResilientClient:
+    def weather_api(self, url, params=None):
+        client = _get_thread_client()
+        try:
+            return client.weather_api(url, params=params)
+        except sqlite3.DatabaseError:
+            _thread_local.client = _build_client()
+            return _thread_local.client.weather_api(url, params=params)
+
</code_context>
<issue_to_address>
**issue (bug_risk):** Catching all sqlite3.DatabaseError and blindly rebuilding the client risks masking persistent cache/DB issues.

Rebuilding the client on any sqlite3.DatabaseError with the same cache path could just hit the same failure again if the DB is corrupted or the filesystem is unhealthy. Consider catching a narrower exception (e.g. OperationalError) and/or adding logic to clear/recreate the cache file, cap the number of rebuild attempts, or ultimately propagate the error so callers can handle a persistent failure appropriately.
</issue_to_address>

### Comment 2
<location path="src/open_meteo.py" line_range="22-25" />
<code_context>


-openmeteo_client = _create_client()
+def _get_thread_client() -> openmeteo_requests.Client:
+    if not hasattr(_thread_local, "client"):
+        _thread_local.client = _build_client()
+    return _thread_local.client
+
+
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Thread-local client recreation on error may leave underlying sessions/connections unclosed.

When `_thread_local.client` is replaced after a `DatabaseError`, the previous client (and its session/cache backend) is discarded without explicit cleanup. If `openmeteo_requests.Client`/`requests_cache` hold SQLite connections or file descriptors, this can leak resources under repeated errors. Consider a helper that calls a `close()`/context-exit on the old client (if available) before assigning a new one.

Suggested implementation:

```python
def _set_thread_client(client: openmeteo_requests.Client) -> openmeteo_requests.Client:
    """Set the thread-local client, closing any existing client if it exposes a close() method.

    This helps avoid leaking resources (e.g. SQLite connections/file descriptors) when
    the thread-local client is recreated after errors.
    """
    old_client = getattr(_thread_local, "client", None)

    if old_client is not None:
        # Best-effort cleanup of the previous client; ignore cleanup errors.
        close = getattr(old_client, "close", None)
        if callable(close):
            try:
                close()
            except Exception:
                # Intentionally ignore to avoid masking the original error path
                pass

    _thread_local.client = client
    return client


def _get_thread_client() -> openmeteo_requests.Client:
    if not hasattr(_thread_local, "client"):
        # First-time initialization for this thread; no previous client to clean up.
        return _set_thread_client(_build_client())
    return _thread_local.client

```

To fully address the resource-leak concern on error-triggered client recreation, any code paths that currently do something like:

```python
_thread_local.client = _build_client()
```

for recovery (e.g. in a `DatabaseError` handler) should be updated to instead call:

```python
_set_thread_client(_build_client())
```

This ensures the old client is explicitly cleaned up before being replaced in all cases, not only during first-time initialization in `_get_thread_client()`.
</issue_to_address>

### Comment 3
<location path="src/api.py" line_range="109-110" />
<code_context>

     # Current values. The order of variables needs to be the same as requested.
     current = response.Current()
+    if current is None:
+        return "No data"
     current_uv_index = round(current.Variables(0).Value(), decimal)

</code_context>
<issue_to_address>
**issue (bug_risk):** Returning a string when `current` is None can make the `get_uv` return type inconsistent and brittle for callers.

This change makes callers handle both numeric and string return values, which is easy to misuse (e.g. arithmetic or formatting expecting a number). Prefer a numeric-compatible sentinel (e.g. `None`, `math.nan`, or a reserved numeric code) or handle the "no data" case at a higher layer so this function always returns a consistent type.
</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/open_meteo.py
openmeteo_client = _create_client()
def _get_thread_client() -> openmeteo_requests.Client:
if not hasattr(_thread_local, "client"):
_thread_local.client = _build_client()
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.

issue (bug_risk): Catching all sqlite3.DatabaseError and blindly rebuilding the client risks masking persistent cache/DB issues.

Rebuilding the client on any sqlite3.DatabaseError with the same cache path could just hit the same failure again if the DB is corrupted or the filesystem is unhealthy. Consider catching a narrower exception (e.g. OperationalError) and/or adding logic to clear/recreate the cache file, cap the number of rebuild attempts, or ultimately propagate the error so callers can handle a persistent failure appropriately.

Comment thread src/open_meteo.py
Comment on lines +22 to +25
def _get_thread_client() -> openmeteo_requests.Client:
if not hasattr(_thread_local, "client"):
_thread_local.client = _build_client()
return _thread_local.client
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): Thread-local client recreation on error may leave underlying sessions/connections unclosed.

When _thread_local.client is replaced after a DatabaseError, the previous client (and its session/cache backend) is discarded without explicit cleanup. If openmeteo_requests.Client/requests_cache hold SQLite connections or file descriptors, this can leak resources under repeated errors. Consider a helper that calls a close()/context-exit on the old client (if available) before assigning a new one.

Suggested implementation:

def _set_thread_client(client: openmeteo_requests.Client) -> openmeteo_requests.Client:
    """Set the thread-local client, closing any existing client if it exposes a close() method.

    This helps avoid leaking resources (e.g. SQLite connections/file descriptors) when
    the thread-local client is recreated after errors.
    """
    old_client = getattr(_thread_local, "client", None)

    if old_client is not None:
        # Best-effort cleanup of the previous client; ignore cleanup errors.
        close = getattr(old_client, "close", None)
        if callable(close):
            try:
                close()
            except Exception:
                # Intentionally ignore to avoid masking the original error path
                pass

    _thread_local.client = client
    return client


def _get_thread_client() -> openmeteo_requests.Client:
    if not hasattr(_thread_local, "client"):
        # First-time initialization for this thread; no previous client to clean up.
        return _set_thread_client(_build_client())
    return _thread_local.client

To fully address the resource-leak concern on error-triggered client recreation, any code paths that currently do something like:

_thread_local.client = _build_client()

for recovery (e.g. in a DatabaseError handler) should be updated to instead call:

_set_thread_client(_build_client())

This ensures the old client is explicitly cleaned up before being replaced in all cases, not only during first-time initialization in _get_thread_client().

Comment thread src/api.py
Comment on lines +109 to +110
if current is None:
return "No data"
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.

issue (bug_risk): Returning a string when current is None can make the get_uv return type inconsistent and brittle for callers.

This change makes callers handle both numeric and string return values, which is easy to misuse (e.g. arithmetic or formatting expecting a number). Prefer a numeric-compatible sentinel (e.g. None, math.nan, or a reserved numeric code) or handle the "no data" case at a higher layer so this function always returns a consistent type.

@ryansurf ryansurf merged commit 7d57b25 into main Jun 2, 2026
10 of 11 checks passed
@ryansurf ryansurf deleted the bugfix-server-crash branch June 2, 2026 22:45
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.

1 participant