Skip to content

Feat cache update#197

Merged
ryansurf merged 4 commits into
mainfrom
feat-cache-update
Apr 23, 2026
Merged

Feat cache update#197
ryansurf merged 4 commits into
mainfrom
feat-cache-update

Conversation

@ryansurf
Copy link
Copy Markdown
Owner

@ryansurf ryansurf commented Apr 23, 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?

Changes

There are two main changes here:

  • src/open_meteo.py:

    • Prior to this, we we're creating a new open meteo client for each API call. This unnecessary duplicated work was impacting response times of the API.
    • This file implements singleton logic (which is the nature of Python modules) - openmeteo_client is imported from other files. The module is only executed once, so openmeteo_client = _create_client() runs exactly once
  • src/api.py:

    • Refactored to use the new openmeteo_client import
    • The gather_data() function makes several API calls, which are synchronous - this was speed up my introducing concurrency via ThreadPoolExecutor, which heavily speeds up the APIs response time

Impact

Response time before the changes (7.9 seconds):

❯ time curl 'localhost:8000?loc=steamer_lane'


Location:  Steamer Lane



      .-``'.
    .`   .`
_.-'     '._
        
UV index: 0.0
Wave Height: nan
Wave Direction: nan
Wave Period: nan


curl 'localhost:8000?loc=steamer_lane'  0.01s user 0.01s system 0% cpu 7.954 total

Response time after the changes (2.5 seconds):

❯ time curl 'localhost:8000?loc=steamer_lane'


Location:  Steamer Lane



      .-``'.
    .`   .`
_.-'     '._
        
UV index: 0.0
Wave Height: nan
Wave Direction: nan
Wave Period: nan


curl 'localhost:8000?loc=steamer_lane'  0.00s user 0.01s system 0% cpu 2.542 total

Summary by Sourcery

Introduce a shared Open-Meteo client and parallelize data aggregation to improve API response performance.

Enhancements:

  • Create a reusable Open-Meteo client module and switch API functions to use the shared client instance.
  • Parallelize multiple Open-Meteo API calls in gather_data using a ThreadPoolExecutor to reduce overall latency.
  • Standardize TTL cache configurations by introducing a shared max-size constant for related caches.

Tests:

  • Update API tests to patch the shared Open-Meteo client instead of a per-call factory and keep coverage aligned with the new design.

@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai Bot commented Apr 23, 2026

Reviewer's Guide

Refactors Open-Meteo client usage into a singleton module and introduces concurrent data fetching in the API layer, while updating caches and tests accordingly.

Sequence diagram for concurrent gather_data execution

sequenceDiagram
    actor User
    participant APILayer as api_gather_data
    participant Executor as ThreadPoolExecutor
    participant Ocean as ocean_information
    participant UV as get_uv
    participant Hourly as get_hourly_forecast
    participant WindTemp as current_wind_temp
    participant Rain as get_rain
    participant Forecast as forecast
    participant OceanHist as ocean_information_history
    participant UVHist as get_uv_history
    participant Client as openmeteo_client
    participant OpenMeteoAPI

    User->>APILayer: gather_data(lat, long, arguments)
    APILayer->>APILayer: parse lat, long, decimal, unit
    APILayer->>Executor: create with max_workers 8

    par submit_ocean
        Executor->>Ocean: submit(lat, long, decimal, unit)
        Ocean->>Client: weather_api(marine_url, params)
        Client->>OpenMeteoAPI: HTTP request
        OpenMeteoAPI-->>Client: marine response
        Client-->>Ocean: responses
    and submit_uv
        Executor->>UV: submit(lat, long, decimal, unit)
        UV->>Client: weather_api(air_quality_url, params)
        Client->>OpenMeteoAPI: HTTP request
        OpenMeteoAPI-->>Client: uv response
        Client-->>UV: responses
    and submit_hourly
        Executor->>Hourly: submit(lat, long)
        Hourly->>Client: weather_api(forecast_url, params)
        Client->>OpenMeteoAPI: HTTP request
        OpenMeteoAPI-->>Client: hourly response
        Client-->>Hourly: responses
    and submit_wind_temp
        Executor->>WindTemp: submit(lat, long, decimal)
        WindTemp->>Client: weather_api(forecast_url, params)
        Client->>OpenMeteoAPI: HTTP request
        OpenMeteoAPI-->>Client: wind temp response
        Client-->>WindTemp: responses
    and submit_rain
        Executor->>Rain: submit(lat, long)
        Rain->>Client: weather_api(forecast_url, params)
        Client->>OpenMeteoAPI: HTTP request
        OpenMeteoAPI-->>Client: rain response
        Client-->>Rain: responses
    and submit_forecast
        Executor->>Forecast: submit(lat, long, decimal, 7)
        Forecast->>Client: weather_api(marine_url, params)
        Client->>OpenMeteoAPI: HTTP request
        OpenMeteoAPI-->>Client: marine forecast
        Client-->>Forecast: responses
        Forecast->>Client: weather_api(general_url, params)
        Client->>OpenMeteoAPI: HTTP request
        OpenMeteoAPI-->>Client: general forecast
        Client-->>Forecast: responses
    and submit_ocean_hist
        Executor->>OceanHist: submit(lat, long, decimal, unit)
        OceanHist->>Client: weather_api(marine_url, params)
        Client->>OpenMeteoAPI: HTTP request
        OpenMeteoAPI-->>Client: marine history response
        Client-->>OceanHist: responses
    and submit_uv_hist
        Executor->>UVHist: submit(lat, long, decimal, unit)
        UVHist->>Client: weather_api(air_quality_url, params)
        Client->>OpenMeteoAPI: HTTP request
        OpenMeteoAPI-->>Client: uv history response
        Client-->>UVHist: responses
    end

    Executor-->>APILayer: futures with results
    APILayer->>APILayer: build results dict
    APILayer-->>User: aggregated surf data dict
Loading

Class diagram for open_meteo singleton client and api module functions

classDiagram
    class OpenMeteoModule {
        +openmeteo_client openmeteo_requests_Client
        +_create_client() openmeteo_requests_Client
    }

    class ApiModule {
        +get_coordinates(args tuple) list_or_str
        +get_uv(lat float, long float, decimal int, unit str) float_or_str
        +get_uv_history(lat float, long float, decimal int, unit str) dict_or_str
        +ocean_information(lat float, long float, decimal int, unit str) tuple
        +ocean_information_history(lat float, long float, decimal int, unit str) tuple
        +current_wind_temp(lat float, long float, temp_unit str) tuple
        +get_rain(lat float, long float) tuple
        +forecast(lat float, long float, decimal int, days int) dict
        +get_hourly_forecast(lat float, long float, days int) dict
        +gather_data(lat float_or_str, long float_or_str, arguments dict) dict
    }

    class TTLCacheOcean {
        +maxsize int
        +ttl int
    }
    class TTLCacheUV {
        +maxsize int
        +ttl int
    }
    class TTLCacheUVHistory {
        +maxsize int
        +ttl int
    }
    class TTLCacheOceanHistory {
        +maxsize int
        +ttl int
    }
    class TTLCacheWindTemp {
        +maxsize int
        +ttl int
    }
    class TTLCacheRain {
        +maxsize int
        +ttl int
    }
    class TTLCacheForecast {
        +maxsize int
        +ttl int
    }
    class TTLCacheHourlyForecast {
        +maxsize int
        +ttl int
    }

    ApiModule --> OpenMeteoModule : uses openmeteo_client
    ApiModule --> TTLCacheOcean : uses
    ApiModule --> TTLCacheUV : uses
    ApiModule --> TTLCacheUVHistory : uses
    ApiModule --> TTLCacheOceanHistory : uses
    ApiModule --> TTLCacheWindTemp : uses
    ApiModule --> TTLCacheRain : uses
    ApiModule --> TTLCacheForecast : uses
    ApiModule --> TTLCacheHourlyForecast : uses
Loading

File-Level Changes

Change Details Files
Introduce a singleton Open-Meteo client shared across the codebase instead of constructing a client per call.
  • Add a dedicated module that creates a single retry-enabled Open-Meteo client at import time using requests.Session and retry.
  • Remove the per-call client factory function from the API module and replace all usages with the shared client import.
src/open_meteo.py
src/api.py
Speed up gather_data by executing multiple external API calls concurrently.
  • Refactor gather_data to use ThreadPoolExecutor to call ocean, UV, hourly forecast, wind/temp, rain, general forecast, and their history endpoints in parallel.
  • Simplify result assembly by consuming the futures into a results dict and building the final response from that dict.
src/api.py
Standardize cache configuration and minor cleanup in API module.
  • Introduce a shared _MAXSIZE constant for TTLCache instances instead of magic numbers.
  • Remove unused imports related to the old Open-Meteo client and request caching.
src/api.py
Update tests to work with the new shared Open-Meteo client and behavior.
  • Change patches from the old client factory to the new openmeteo_client symbol in the API module.
  • Adjust tests that simulated client behavior to work with the direct client mocks (no factory return), including error-path tests.
tests/test_api.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

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 23, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

Files with missing lines Coverage Δ
src/api.py 100.00% <100.00%> (ø)
src/open_meteo.py 100.00% <100.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 2 issues, and left some high level feedback:

  • In gather_data, consider deriving max_workers from the number of submitted tasks or a module-level constant (and possibly reusing a shared executor) rather than hardcoding 8, to make concurrency behavior clearer and easier to tune.
  • The eager, module-level construction of openmeteo_client in open_meteo.py couples import time to network client setup; you might want to use a lazily initialized singleton or a small accessor function (e.g. get_openmeteo_client()) to defer creation and make future configuration/injection easier.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `gather_data`, consider deriving `max_workers` from the number of submitted tasks or a module-level constant (and possibly reusing a shared executor) rather than hardcoding `8`, to make concurrency behavior clearer and easier to tune.
- The eager, module-level construction of `openmeteo_client` in `open_meteo.py` couples import time to network client setup; you might want to use a lazily initialized singleton or a small accessor function (e.g. `get_openmeteo_client()`) to defer creation and make future configuration/injection easier.

## Individual Comments

### Comment 1
<location path="src/api.py" line_range="510-519" />
<code_context>
+    dec, unit = arguments["decimal"], arguments["unit"]
+
+    with ThreadPoolExecutor(max_workers=8) as executor:
+        futures = {
+            "ocean": executor.submit(ocean_information, lat, long, dec, unit),
+            "uv": executor.submit(get_uv, lat, long, dec, unit),
+            "hourly": executor.submit(get_hourly_forecast, lat, long),
+            "wind_temp": executor.submit(current_wind_temp, lat, long, dec),
+            "rain": executor.submit(get_rain, lat, long),
+            "forecast": executor.submit(forecast, lat, long, dec, 7),
+            "ocean_hist": executor.submit(
+                ocean_information_history, lat, long, dec, unit
+            ),
+            "uv_hist": executor.submit(get_uv_history, lat, long, dec, unit),
+        }
+        results = {k: f.result() for k, f in futures.items()}
</code_context>
<issue_to_address>
**issue (bug_risk):** Using shared global client and caches from multiple threads may introduce thread-safety issues.

These functions share a single `openmeteo_client` (`requests.Session`) and several `TTLCache` instances, neither of which are inherently thread-safe. Calling them in parallel against shared instances can cause race conditions. If you keep this parallelism, either instantiate separate clients/caches per task/thread or protect shared mutable state with appropriate locking.
</issue_to_address>

### Comment 2
<location path="src/open_meteo.py" line_range="6-9" />
<code_context>
+from retry_requests import retry
+
+
+def _create_client() -> openmeteo_requests.Client:
+    """Creates a retry-enabled Open-Meteo API client."""
+    retry_session = retry(requests.Session(), retries=5, backoff_factor=0.2)
+    return openmeteo_requests.Client(session=retry_session)
+
+
</code_context>
<issue_to_address>
**question (performance):** The new client drops the previous HTTP-level caching, which may significantly increase API traffic and latency.

Previously this client used `requests_cache.CachedSession` with a 1-hour TTL, which avoided repeated calls for identical parameters. The new version uses a plain `requests.Session` with retries only. If this behavior change isn’t intentional, consider adding caching back at this layer or documenting that we now expect increased network usage, higher latency, and more exposure to rate limits.
</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/api.py
Comment on lines +510 to +519
futures = {
"ocean": executor.submit(ocean_information, lat, long, dec, unit),
"uv": executor.submit(get_uv, lat, long, dec, unit),
"hourly": executor.submit(get_hourly_forecast, lat, long),
"wind_temp": executor.submit(current_wind_temp, lat, long, dec),
"rain": executor.submit(get_rain, lat, long),
"forecast": executor.submit(forecast, lat, long, dec, 7),
"ocean_hist": executor.submit(
ocean_information_history, lat, long, dec, unit
),
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): Using shared global client and caches from multiple threads may introduce thread-safety issues.

These functions share a single openmeteo_client (requests.Session) and several TTLCache instances, neither of which are inherently thread-safe. Calling them in parallel against shared instances can cause race conditions. If you keep this parallelism, either instantiate separate clients/caches per task/thread or protect shared mutable state with appropriate locking.

Comment thread src/open_meteo.py
Comment on lines +6 to +9
def _create_client() -> openmeteo_requests.Client:
"""Creates a retry-enabled Open-Meteo API client."""
retry_session = retry(requests.Session(), retries=5, backoff_factor=0.2)
return openmeteo_requests.Client(session=retry_session)
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.

question (performance): The new client drops the previous HTTP-level caching, which may significantly increase API traffic and latency.

Previously this client used requests_cache.CachedSession with a 1-hour TTL, which avoided repeated calls for identical parameters. The new version uses a plain requests.Session with retries only. If this behavior change isn’t intentional, consider adding caching back at this layer or documenting that we now expect increased network usage, higher latency, and more exposure to rate limits.

@ryansurf ryansurf merged commit be38500 into main Apr 23, 2026
11 checks passed
@ryansurf ryansurf deleted the feat-cache-update branch April 23, 2026 01:43
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