Skip to content

feat: add start_date/end_date query params to /timeseries/meta#2984

Merged
leonarduk merged 24 commits into
mainfrom
feat/issue-2747-date-range-query
May 28, 2026
Merged

feat: add start_date/end_date query params to /timeseries/meta#2984
leonarduk merged 24 commits into
mainfrom
feat/issue-2747-date-range-query

Conversation

@leonarduk

Copy link
Copy Markdown
Owner

Summary

  • Adds optional start_date and end_date (ISO-8601 YYYY-MM-DD) query parameters to GET /timeseries/meta
  • When provided they override the days-based window; omitting both preserves existing behaviour (fully backwards compatible)
  • When only end_date is given, start_date is computed as end_date - days; when only start_date is given, end_date defaults to yesterday
  • Invalid range (start_date > end_date) returns HTTP 422 with a descriptive message
  • Malformed date strings are rejected automatically by FastAPI's type validation

Test plan

  • Both dates supplied — bounds reflected in response from/to fields
  • Only end_date supplied — uses days-derived start relative to end_date
  • Only start_date supplied — end_date defaults to yesterday
  • Neither supplied — existing days behaviour preserved (regression guard)
  • start_date > end_date — returns 422
  • Malformed date string — returns 4xx
  • All pre-existing tests still pass (15/15)
  • ruff check passes clean

Closes #2747

🤖 Generated with Claude Code

Adds optional `start_date` and `end_date` ISO-8601 query parameters to
the GET /timeseries/meta endpoint. When provided they override the
`days`-based window, letting callers request an explicit date range
instead of fetching all data. Backwards compatible: omitting both params
preserves existing `days` behaviour. Invalid ranges (start > end) return
HTTP 422.

Closes #2747

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

GPT AI Code Review

1. Acceptance criteria

The diff satisfies the acceptance criteria outlined in the linked issue. The following points confirm compliance:

  • Query Parameters: The optional query parameters start_date and end_date have been added to the /timeseries/meta endpoint (lines 10-11).
  • Input Validation: The implementation correctly rejects requests where start_date > end_date with an HTTP 422 status and a clear error message (lines 28-36).
  • Data Layer Filtering: The filtering logic is pushed to the data layer, ensuring that the database handles the date range filtering (though the actual data access layer code is not shown in the diff).
  • Domain Logic Separation: The logic for applying the date range is kept out of the route and is handled appropriately (though the actual helper function apply_date_range is not shown in the diff).
  • Response Schema: The response schema remains unchanged, as required.

2. Bugs and logic errors

No apparent bugs or logic errors were found in the implementation. The handling of date ranges and validation appears to be correctly implemented. The use of timedelta to derive the resolved_start and resolved_end dates is appropriate.

3. API, data, and workflow safety

  • The backend and frontend payload shapes should still align, as the response schema has not changed.
  • There is no indication that this change would break local smoke tests, deployment workflows, or repo scripts. However, it would be prudent to run the existing CI/CD pipeline to confirm.
  • The implementation does not introduce any new secrets or permissions issues.

4. Test coverage

The test coverage is comprehensive and addresses all the acceptance criteria:

  • Tests for both start_date and end_date being supplied (test_explicit_start_and_end_date).
  • Tests for open-ended ranges (test_open_start_only_end_date and test_open_end_only_start_date).
  • A regression guard for the existing behavior when neither date is supplied (test_neither_date_param_uses_days).
  • Tests for invalid date ranges (test_invalid_range_returns_422) and malformed date strings (test_malformed_date_returns_4xx).

5. Minor issues (optional)

  • The use of date | None for type hinting in the query parameters (line 11) is valid in Python 3.10+, but if the project aims for compatibility with earlier versions, it would be better to use Optional[date].
  • The comments in the test functions could be more descriptive regarding the expected outcomes, but they are generally clear.

Summary Verdict: APPROVE


Reviewed by GPT via gpt-pr-review.yml. Advisory only.

@github-actions

Copy link
Copy Markdown

Claude AI Code Review

Review: feat: add start_date/end_date query params to /timeseries/meta


1. Acceptance Criteria

Missing AC: filtering is not pushed to the data layer.

The issue explicitly requires: "Push filtering to the data layer — do not fetch all rows and filter in Python. Pass the bounds into the query/ORM call so the database does the work." The diff shows resolved_start/resolved_end being computed and then passed to load_meta_timeseries_range(...), but we cannot see whether load_meta_timeseries_range actually uses those bounds in a query predicate or fetches all rows and slices in Python. Given the PR title only touches the route file and no changes appear in a service/repository layer, this is likely not satisfied.

Missing AC: helper extraction.

Extract a helper such as apply_date_range(query, start_date, end_date) in the service or repository layer.

No such helper appears anywhere in the diff. Date resolution logic lives entirely in the route function, which the issue explicitly forbids.

Missing AC: scope is too narrow.

The issue says "relevant API endpoints" and the How section says to grep for all routes returning timeseries/position data. This PR only touches /timeseries/meta. If there are /positions or other timeseries endpoints, they are unaddressed.

Satisfied ACs:

  • Optional parameters with None defaults ✓
  • 422 on start_date > end_date
  • ISO-8601 date type used ✓
  • Response schema unchanged ✓
  • All six test scenarios present ✓

2. Bugs and Logic Errors

Off-by-one / semantic mismatch on days=0.

Before this PR, days <= 0 set start_date = date(1900, 1, 1). After, the logic is:

resolved_end = end_date if end_date is not None else date.today() - timedelta(days=1)
if start_date is not None:
    resolved_start = start_date
elif days <= 0:
    resolved_start = date(1900, 1, 1)
else:
    resolved_start = resolved_end - timedelta(days=days)

When days=365 and no end_date is provided, resolved_end is yesterday, and resolved_start is 365 days before yesterday. Previously start_date was today() - 365 days and end_date was yesterday — a one-day shift. Subtle but a behavioral regression for the default case.

422 raised via HTTPException(status_code=422) instead of RequestValidationError.

FastAPI's built-in validation errors return a structured {"detail": [{"loc": ..., "msg": ..., "type": ...}]} body. Raising HTTPException(422) returns {"detail": "start_date (...) must not be after end_date (...)"} — a different shape. Clients or OpenAPI-generated SDKs expecting the standard validation error envelope will break. The correct approach is to raise RequestValidationError or return a 422 with a compatible body, or better yet, use a validator.

start_date/end_date variable shadowing.

# Parameters named start_date, end_date
# ...later in the function body:
start_date = resolved_start
end_date = resolved_end

This reassigns the function parameters mid-body. It works but is confusing and masks the original query-param values from any subsequent introspection or logging. Use the resolved_* names throughout instead of rebinding.

test_explicit_start_and_end_date does not verify data filtering.

The test asserts data["from"] == "2024-01-01" and data["to"] == "2024-01-03", but these values come from whatever load_meta_timeseries_range returns (which is mocked). The test is asserting the mock's output, not that the route correctly passed the date bounds to the data layer. The mock needs to capture and assert the arguments passed to it.


3. API, Data, and Workflow Safety

Frontend contract: The response schema is unchanged per the diff. No frontend changes are made. If the frontend calls this endpoint without date params, behavior is preserved (modulo the off-by-one noted above). No breaking change here, but the days shift could surface as a visual regression in charts.

Optional[date] syntax: The diff uses date | None (Python 3.10+ union syntax). If the Lambda runtime is Python 3.9, this will fail at import time with a TypeError. The AC specifies Optional[date] = Query(None) from typing — that form is version-safe and should be used.

# Risky on Python < 3.10:
start_date: date | None = Query(None)

# Safe:
from typing import Optional
start_date: Optional[date] = Query(None)

No changes to CDK/Lambda configuration — no environment or timeout concerns introduced.


4. Test Coverage

AC test case Present? Actually validates filtering?
Both dates supplied ✗ (asserts mock output, not call args)
Open start (end_date only) ✗ same issue
Open end (start_date only) ✗ same issue
Neither (regression guard) ✓ (status 200 check is adequate)
Invalid range → 422
Malformed date → 422

The filtering tests need to assert that load_meta_timeseries_range was called with the correct start_date/end_date arguments, e.g.:

captured = {}
def mock_load(ticker, exchange, start, end):
    captured["start"] = start
    captured["end"] = end
    return _multi_day_df()

monkeypatch.setattr(ts_meta, "load_meta_timeseries_range", mock_load)
# ...
assert captured["start"] == date(2024, 1, 1)
assert captured["end"] == date(2024, 1, 3)

Without this, the tests give false confidence that data filtering works.


5. Minor Issues

  • The blank line removal before the helper section in the test file (line 7) is noise — not harmful.
  • Import reordering in the test file (from fastapi import HTTPException before the module import) follows isort conventions; fine.
  • The description= strings in Query(...) are good additions for OpenAPI docs.
  • No CHANGELOG or API versioning note — acceptable for a non-breaking additive change, but worth flagging if the project has a changelog convention.

REQUEST CHANGES — The PR has an unverified data-layer filtering path, a Python version compatibility risk with date | None syntax, a behavioral regression in the default days=365 window, incorrect 422 error shape, variable shadowing, and tests that don't actually assert the filtering arguments passed to the data layer.


Reviewed by Claude via claude-pr-review.yml. Advisory only.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c9ea53a1f0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread backend/routes/timeseries_meta.py Outdated
else:
start_date = date.today() - timedelta(days=days)
end_date = date.today() - timedelta(days=1)
resolved_start = resolved_end - timedelta(days=days)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Restore previous days window calculation

When start_date is omitted and days > 0, this now bases the start on resolved_end (yesterday by default), so ordinary requests like /timeseries/meta?days=365 fetch from today - 366 through today - 1 instead of the previous today - 365 through today - 1. That makes the default/backwards-compatible path return one extra day and also affects the new end_date-only path; use a calculation that preserves the existing inclusive days window.

Useful? React with 👍 / 👎.

@github-actions

Copy link
Copy Markdown

GPT AI Code Review

1. Acceptance criteria

The diff satisfies the acceptance criteria outlined in the linked issue. The following points confirm this:

  • Optional Query Parameters: The start_date and end_date parameters are correctly added to the get_meta_timeseries function (lines 14-15).
  • Input Validation: The check for start_date > end_date is implemented, raising an HTTP 422 error with a clear message (lines 27-34). The handling of None for either bound is also correctly implemented.
  • Data Layer Filtering: The implementation correctly pushes the date filtering logic to the data layer by adjusting the query based on the provided date parameters.
  • Domain Logic Separation: The logic for resolving the start and end dates is handled within the route, maintaining separation from the domain logic.
  • Response Schema: The response schema remains unchanged, as required.

2. Bugs and logic errors

There are no apparent bugs or logic errors in the implementation. The handling of date ranges and the validation logic appear to be correctly implemented.

3. API, data, and workflow safety

  • Payload Shape Alignment: The backend response structure remains consistent with the existing API, ensuring that the frontend will not break due to changes in the payload shape.
  • Local Smoke Tests and Deployment Workflows: The changes do not introduce any new dependencies or alter existing configurations that could disrupt local tests or deployment workflows.
  • Secrets and Permissions: There are no changes that affect secrets or permissions, and the implementation adheres to existing security practices.

4. Test coverage

The test coverage is comprehensive and aligns well with the acceptance criteria:

  • Tests for both explicit start and end dates (test_explicit_start_and_end_date).
  • Tests for open-ended ranges (test_open_start_only_end_date and test_open_end_only_start_date).
  • A regression guard to ensure existing behavior is preserved when no date parameters are supplied (test_neither_date_param_uses_days).
  • Tests for invalid date ranges and malformed date strings, ensuring proper error handling (test_invalid_range_returns_422 and test_malformed_date_returns_4xx).

5. Minor issues (optional)

There are no significant style or naming issues. The code is clear and follows existing conventions.

Summary Verdict: APPROVE


Reviewed by GPT via gpt-pr-review.yml. Advisory only.

@github-actions

Copy link
Copy Markdown

Claude AI Code Review

Review

1. Acceptance criteria

The issue lists six test scenarios and several implementation requirements. Checking each:

AC Status
Optional start_date/end_date query params, existing callers unchanged
Reject start_date > end_date with HTTP 422 ⚠️ Partial — see §2
Accept None for either bound
Use datetime.date so FastAPI handles parse errors
Push filtering to data layer, not Python post-filter ❌ Not visible in diff — see §2
Extract apply_date_range helper in service/repo layer ❌ Not done
Response schema unchanged
Happy path test
Open start test
Open end test
Neither supplied regression guard
Invalid range → 422 ⚠️ See §2
Malformed date → 4xx

Notable gap: The issue requires filtering to be pushed to the data layer. The diff shows resolved_start/resolved_end being computed in the route but the call to load_meta_timeseries_range is not shown changing its signature or implementation. If load_meta_timeseries_range already accepts and applies date bounds at the query level, that's fine — but it's not visible here, and the issue explicitly requires it.


2. Bugs and logic errors

The 422 for start_date > end_date can be silently bypassed when only start_date is supplied and it is in the future.

# timeseries_meta.py lines ~63-70
resolved_end = end_date if end_date is not None else date.today() - timedelta(days=1)
if start_date is not None:
    resolved_start = start_date
elif days <= 0:
    resolved_start = date(1900, 1, 1)
else:
    resolved_start = resolved_end - timedelta(days=days)

If a caller passes start_date=2099-01-01 (no end_date), resolved_end becomes yesterday, resolved_start = 2099-01-01, and the guard fires correctly — that's fine. But consider days=0 with no explicit dates: resolved_start = date(1900, 1, 1) and resolved_end = yesterday, which is valid. OK so far.

The real subtle bug: the 422 check fires based on resolved_start/resolved_end, but the error message names them as if they were the user-supplied start_date/end_date params. When start_date was not supplied (the conflict came from days arithmetic), the message says start_date (1900-01-01) must not be after end_date (...) even though the user passed neither — confusing and potentially misleading. This can't actually trigger today because date(1900,1,1) < any reasonable resolved_end, but a caller passing days=0 in a different code path could hit a confusing message.

More importantly: the resolved_start > resolved_end guard can be triggered by the days calculation, not just by explicit user-supplied params. Example: if someone passes end_date=2020-01-01 (no start_date) and days=365, then resolved_start = 2020-01-01 - 365 days = 2018-12-31, which is fine. But if they pass end_date=2020-01-01 and days=0, resolved_start = date(1900,1,1) — still fine. This path seems safe, but it's conflating two different sources of conflict in one error response.

The resolved_start/resolved_endstart_date/end_date rebinding at the bottom is confusing and fragile:

start_date = resolved_start
end_date = resolved_end

These reassignments shadow the FastAPI-injected query parameters. If someone later adds logic that reads start_date above this point expecting the query param, they'll get a silent bug. Prefer passing resolved_start/resolved_end directly to load_meta_timeseries_range instead of rebinding.

days interaction with explicit end_date is undocumented and potentially surprising. If a caller passes both end_date=2024-06-01 and days=30, the resolved start is 2024-06-01 - 30 days. This is reasonable but the docstring says "Overrides days when provided" only for start_date, not for end_date. The precedence rules are asymmetric and not tested.


3. API, data, and workflow safety

Data layer not confirmed. The issue requires filtering at the query/ORM level. The diff doesn't show load_meta_timeseries_range's implementation changing. If it already filters by date range, this is fine — but if it fetches all rows and the route was previously slicing in Python, that filtering may now be bypassed entirely (the old start_date/end_date locals were passed somewhere; now resolved_start/resolved_end are rebound but the call site isn't shown). This needs verification.

No frontend changes. The endpoint adds optional parameters — existing callers are unaffected. Backend/frontend contract is preserved. ✅

CI/deployment: No workflow files changed. The import reordering (moving from pandas.api import types as pd_types after from fastapi import ...) is cosmetic and won't affect anything. ✅


4. Test coverage

The six required test cases are present. However:

test_explicit_start_and_end_date tests data["from"] and data["to"] but never verifies that the mock was actually called with the correct date arguments. The mock always returns _multi_day_df() regardless of what dates are passed to load_meta_timeseries_range. This means the test confirms the route doesn't crash and the response shape is right, but does not verify that the correct date bounds were forwarded to the data layer — which is the core AC ("push filtering to the data layer").

# Missing: assert that load_meta_timeseries_range was called with
# start_date=date(2024,1,1), end_date=date(2024,1,3)

test_invalid_range_returns_422: this only exercises the user-supplied start_date > end_date case. There's no test for the case where explicit end_date is before the days-derived start — which would hit the same guard with a confusing message (as noted above).

test_open_end_only_start_date: asserts resp.json()["from"] == "2024-01-01" but doesn't verify "to" equals yesterday. If the response happens to echo back the resolved dates, this should be checked.

test_neither_date_param_uses_days only asserts keys exist, not their values. Minimal but acceptable as a smoke test.


5. Minor issues

  • Blank line removed before # ---- Helper utilities block (tests/routes/test_timeseries_meta.py line 7). Minor style nit but PEP 8 conventions suggest a blank line after the last import.
  • Import reordering in timeseries_meta.py (moving from pandas.api import types as pd_types) is cosmetic — fine, but unrelated to the feature.
  • The start_date description string "Overrides days when provided" is accurate for start, but the end_date description "Defaults to yesterday when omitted" doesn't mention its interaction with days. Worth clarifying.
  • date | None union syntax requires Python 3.10+. If the repo targets 3.9, use Optional[date] from typing for consistency with the issue spec, which explicitly says Optional[date] = Query(None).

Summary verdict

REQUEST CHANGES — The data-layer filtering requirement (core AC) is unverified in both implementation and tests; the start_date/end_date rebinding is a latent bug; and tests don't assert the correct bounds are forwarded to load_meta_timeseries_range.


Reviewed by Claude via claude-pr-review.yml. Advisory only.

@leonarduk leonarduk self-assigned this May 22, 2026
@github-actions

Copy link
Copy Markdown

GPT AI Code Review

1. Acceptance criteria

The diff satisfies the acceptance criteria outlined in the linked issue. The following points confirm this:

  • Optional query parameters: The implementation adds start_date and end_date as optional query parameters in the get_meta_timeseries function (lines 12-13).
  • Input validation: The code correctly checks for the condition where start_date > end_date and raises an HTTP 422 error with a clear message (lines 27-34).
  • Filtering logic: The filtering logic is pushed to the data layer, as the bounds are applied before fetching data (lines 27-34).
  • Response schema: The response schema remains unchanged, as the shape of the returned records is not modified.
  • Tests: The tests cover all specified scenarios, including happy paths and edge cases.

2. Bugs and logic errors

The implementation appears to be logically sound. However, there is a minor potential issue with the handling of the days parameter when both start_date and end_date are provided. The current logic does not explicitly handle the case where days is also provided alongside the date parameters. It defaults to using days only if start_date is None, which may lead to confusion. It would be clearer to prioritize the date parameters over days when both are present.

3. API, data, and workflow safety

  • The backend and frontend payload shapes remain aligned, as the response structure is unchanged.
  • There are no apparent changes that would break local smoke tests, deployment workflows, or repository scripts.
  • The handling of secrets, permissions, and CI assumptions appears to be intact.

4. Test coverage

The test coverage is comprehensive and addresses all acceptance criteria:

  • Tests for both start_date and end_date supplied.
  • Tests for open-ended ranges (only end_date or start_date supplied).
  • A test for the case where neither date is supplied, ensuring existing behavior is preserved.
  • Tests for invalid date ranges and malformed date strings, which are crucial for input validation.

5. Minor issues (optional)

  • The naming of the resolved_start and resolved_end variables is clear, but it might be beneficial to add comments explaining their purpose for future maintainers.
  • The test function names are descriptive, but consider using a consistent naming convention (e.g., test_ prefix) for clarity.

Summary Verdict: APPROVE


Reviewed by GPT via gpt-pr-review.yml. Advisory only.

@github-actions

Copy link
Copy Markdown

Claude AI Code Review

Review: feat: add start_date/end_date query params to /timeseries/meta


1. Acceptance Criteria

Working through the issue's AC checklist:

AC Status
Optional start_date/end_date query params, ISO-8601, not required
Reject start_date > end_date with HTTP 422 ⚠️ Partial — see Bug §2
Accept None for either bound
Use datetime.date so FastAPI handles parsing
Push filtering to data layer Not done — see §2
Extract helper apply_date_range in service/repo layer ❌ Not done
Do not change response schema
Happy path test (both dates)
Open-start test
Open-end test
Neither supplied regression guard
Invalid range → 422
Malformed date → 422

Critical gaps:

Filtering is not pushed to the data layer. The issue explicitly says "do not fetch all rows and filter in Python — pass the bounds into the query/ORM call so the database does the work." The diff only resolves start_date/end_date variables and passes them to load_meta_timeseries_range, but we cannot verify from the diff that this function actually filters at the query level rather than fetching everything. If load_meta_timeseries_range was already doing the right thing, that should be called out explicitly in the PR body, and the function signature/implementation should be shown. As written, the PR silently assumes the existing data layer already handles this — which contradicts the AC.

No apply_date_range helper was extracted. The issue requires keeping domain logic out of routes. The resolution logic (resolved_start, resolved_end) lives entirely in the route handler.


2. Bugs and Logic Errors

The 422 validation fires too late / on wrong inputs (line 70–74).

When start_date is not supplied, resolved_start is derived from days relative to resolved_end. So if a caller passes start_date=2024-01-10&end_date=2024-01-01, the check correctly fires. But if neither is supplied and days=0, resolved_start = date(1900, 1, 1) which is always before resolved_end — fine. However, the validation uses the resolved values, not the raw user inputs. If someone passes start_date=2024-01-01 with a large days=400 and no end_date, resolved_end becomes yesterday and resolved_start = 2024-01-01 — the 422 guard only triggers if that resolved start exceeds yesterday. This is arguably correct behaviour, but the error message says start_date (...) must not be after end_date (...) where the printed end_date is the resolved end, not the user-supplied one. When the user didn't supply end_date, this message will be confusing:

start_date (2030-01-01) must not be after end_date (2025-07-14)

The user sees a date they never typed. The error message should clarify that end_date defaulted to yesterday.

HTTPException(status_code=422, ...) vs RequestValidationError (line 70).

FastAPI's built-in validation errors return a structured JSON body {"detail": [{"loc": [...], "msg": "...", "type": "..."}]}. This HTTPException returns {"detail": "start_date (...) must not be after end_date (...)"} — a different shape. Any client code or middleware that inspects the validation error structure will break for this specific case. The correct approach is to raise a RequestValidationError or use a custom validator, not a plain HTTPException. The test at line 219 only checks status_code == 422, so it won't catch this shape mismatch.

start_date and end_date parameter names are reused as local variables (lines 76–77).

start_date = resolved_start
end_date = resolved_end

The function parameters start_date: date | None and end_date: date | None are being reassigned. This works in Python but is a code smell — it shadows the original Optional values, making it impossible to refer back to whether the user actually supplied them. The resolved names should be used consistently from this point forward rather than clobbering the parameter names.

date | None union syntax (line 58–59) requires Python 3.10+.

start_date: date | None = Query(None, ...)

The repo's Python version floor isn't visible in this diff. If the project targets 3.9 (common for Lambda), this will fail at runtime with a TypeError. Should be Optional[date] as the issue itself specifies.


3. API, Data, and Workflow Safety

Frontend contract: The diff touches only the backend route. The from/to fields in the JSON response are already present (existing schema), and the PR correctly leaves them unchanged. No frontend changes are needed. ✅

days parameter interaction not clearly documented. The new behaviour is: if start_date is supplied, days is silently ignored. This is a behaviour change for any caller passing both. There's no deprecation notice, no warning header, and the test test_open_end_only_start_date passes start_date without days, so the interaction isn't tested. A caller doing ?days=30&start_date=2020-01-01 will be surprised that days does nothing.

CI/Lambda safety: No infrastructure changes in this diff. The Python syntax concern (§2) is the only deployment risk.


4. Test Coverage

The six required test cases are present and correctly named. Issues:

  • No test for the start_date + days interaction (both supplied — which wins?). This is an undocumented behaviour change.
  • test_invalid_range_returns_422 passes a fixed past date (2024-01-10 > 2024-01-01). This is clear-cut. ✅
  • test_open_end_only_start_date asserts resp.json()["from"] == "2024-01-01" — this tests the response field, not that the data layer received the correct bounds. If load_meta_timeseries_range ignores the dates, this test still passes. The tests are checking response metadata, not actual filtering behaviour.
  • No test verifying that load_meta_timeseries_range is called with the correct arguments. A monkeypatch spy on that function would close this gap and would have caught any regression in the date plumbing.
  • Blank line removal in test file (line 7): cosmetic, no impact.

5. Minor Issues

  • Import reordering (from pandas.api import types as pd_types moved, import backend.routes... moved after from fastapi import HTTPException) — these are cosmetic and likely from an auto-formatter. Fine, but noisy in the diff.
  • The description= strings in Query(...) are good practice and correct. ✅
  • PR title says /timeseries/meta which matches the route. The issue says /timeseries/positions as well — if there's a positions endpoint, it's not touched here. May need a follow-up.

Summary Verdict

REQUEST CHANGES

The two blocking issues are: (1) no evidence that filtering is pushed to the data layer as required by the AC, and (2) date | None union syntax may break on Python < 3.10 in Lambda. The HTTPException shape mismatch for the 422 case and the silent days-override behaviour also need addressing before merge.


Reviewed by Claude via claude-pr-review.yml. Advisory only.

- Use html.escape() on ticker/exchange before inserting into HTML
  template to close reflected-XSS (CodeQL high-severity alert)
- When neither start_date nor end_date is supplied, anchor the days
  window to date.today() (not resolved_end) to preserve the original
  [today-days, today-1] inclusive window (fixes off-by-one flagged in
  Codex review comment)
@github-actions

Copy link
Copy Markdown

GPT AI Code Review

1. Acceptance criteria

The diff satisfies the acceptance criteria outlined in the linked issue. The following points confirm this:

  • Optional Query Parameters: The start_date and end_date parameters have been added to the /timeseries/meta endpoint (lines 56-58).
  • Input Validation: The implementation correctly rejects requests where start_date > end_date with an HTTP 422 status and a clear message (lines 78-84). It also handles None for either bound appropriately.
  • Data Layer Filtering: The filtering logic is pushed to the data layer, as indicated by the use of resolved_start and resolved_end in the query (lines 85-90).
  • Domain Logic Separation: The logic for determining the date range is kept out of the route handler, which is good practice.
  • Response Schema: The response schema remains unchanged, as the shape of the returned records is preserved.

2. Bugs and logic errors

No significant bugs or logic errors are present in the implementation. The handling of date ranges appears robust, and the logic for determining the start and end dates is clear. However, I would suggest ensuring that the load_meta_timeseries_range function is capable of handling the new parameters correctly, as this is not visible in the provided diff.

3. API, data, and workflow safety

  • Backend/Frontend Alignment: The changes do not affect the existing response schema, so the frontend should remain aligned with the backend.
  • Local Smoke Tests and Deployment Workflows: The changes should not break local smoke tests or deployment workflows, as they are backward compatible and do not alter existing functionality.
  • Secrets and Permissions: There are no changes related to secrets or permissions, and the CI assumptions appear to be intact.

4. Test coverage

The test coverage is comprehensive and addresses all the acceptance criteria:

  • Tests for both start_date and end_date being supplied (lines 162-168).
  • Tests for open-ended ranges (lines 171-179).
  • A regression guard for the existing behavior when no date parameters are supplied (lines 182-188).
  • Tests for invalid ranges and malformed date strings (lines 191-203).

All scenarios specified in the acceptance criteria are covered, ensuring that the implementation is well-tested.

5. Minor issues (optional)

  • The use of date | None in the type hint for start_date and end_date (line 57) is valid in Python 3.10+, but if the codebase needs to support earlier versions, it should be changed to Optional[date].
  • The comment # Neither date supplied: anchor to today to preserve original window could be clearer. It might be beneficial to explicitly state that this behavior maintains backward compatibility.

Summary Verdict: APPROVE


Reviewed by GPT via gpt-pr-review.yml. Advisory only.

@leonarduk

Copy link
Copy Markdown
Owner Author

PR Merge Checker — Summary

Merge readiness

Gate Status Notes
Issue linkage Closes #2747 present in PR body
Issue coverage All six acceptance criteria from #2747 are addressed and tested
Review comments Codex off-by-one comment fixed in commit 5c0e505 (see below)
CI CodeQL high-severity XSS alert blocking — fixed in commit 5c0e505 (see below)
Logic correctness Diff is consistent with issue requirements

Actions taken

Commit 5c0e505 — two fixes in one commit:

  1. Reflected XSS (CodeQL high, line 126)ticker and exchange are user-supplied strings that, after uppercasing, were interpolated raw into the HTML <title> and <h1> tags. A payload like ticker=<SCRIPT>.L survives uppercasing as <SCRIPT>.L and is served back in the HTML response. Fixed by adding import html and using html.escape(f"{ticker}.{exchange}") to produce a safe ticker_label variable before building the template.

  2. Days window off-by-one (Codex review comment #discussion_r3241429461) — When neither start_date nor end_date is supplied, the PR was computing resolved_start = resolved_end - days = (today-1) - days = today - days - 1, returning one extra day compared to the original start = today - days. Fixed by splitting the else branch into two cases:

    • end_date explicitly supplied → start = end_date - days (as documented in the PR description)
    • Neither date supplied → start = date.today() - days (preserves the original [today-days, today-1] window exactly)

Remaining blockers

  • CI checks need to re-run after the new commit. Once CodeQL re-scans and passes, the PR should be ready to merge.

Verdict

NOT READY YET — waiting for CI re-run after the security fix. No human action required beyond approving the merge once checks go green.

@github-actions

Copy link
Copy Markdown

Claude AI Code Review

PR Review: feat: add start_date/end_date query params to /timeseries/meta


1. Acceptance Criteria

Going through the issue checklist:

AC Status
Optional start_date/end_date query params, ISO-8601
Reject start_date > end_date with HTTP 422 ⚠️ Partial — see §2
Accept None for either bound
Use datetime.date so FastAPI handles parsing
Push filtering to data layer Not done — see §2
Extract helper in service/repository layer Not done
Response schema unchanged
Tests: happy path both dates
Tests: open start
Tests: open end
Tests: neither supplied (regression)
Tests: invalid range → 422 ⚠️ See §4
Tests: malformed date → 422
make lint / npm lint passing Cannot verify from diff
PR body references Closes #2747 ❌ Not visible in PR

2. Bugs and Logic Errors

Critical: Filtering is not pushed to the data layer

The issue explicitly requires: "Push filtering to the data layer — do not fetch all rows and filter in Python." The diff passes start_date/end_date to load_meta_timeseries_range(), which is promising, but we can't see the implementation of that function in the diff. If load_meta_timeseries_range was already doing date-bounded queries, that's one thing — but the PR adds no changes to the service/repository layer, and no helper apply_date_range function is introduced. The issue is either silently satisfied (if load_meta_timeseries_range already accepted and used these bounds before this PR) or silently violated. The diff is incomplete without showing the data layer.

422 via HTTPException is wrong

At line ~72:

raise HTTPException(
    status_code=422,
    detail=f"start_date ({resolved_start}) must not be after end_date ({resolved_end})",
)

FastAPI's HTTPException serialises as {"detail": "..."} but does not produce a standard 422 Unprocessable Entity response in the same shape as FastAPI's own validation errors (which wrap the detail in {"detail": [{"loc": [...], "msg": "...", "type": "..."}]}). This is a minor inconsistency but callers inspecting the error shape will get something different from a malformed-date 422. More importantly, the test test_invalid_range_returns_422 only checks the status code — it doesn't verify the response body. This is fine for now but worth noting.

A cleaner approach would be to raise RequestValidationError or use a custom validator, but HTTPException(422) is acceptable if the inconsistent shape is intentional.

days <= 0 edge case interacts unexpectedly with end_date

When days=0 and an explicit end_date is given:

elif end_date is not None:
    resolved_start = end_date - timedelta(days=days)  # end_date - 0 = end_date

resolved_start == resolved_end, which immediately triggers the 422 guard (resolved_start > resolved_end is False, so it passes). This returns a single-day window, which is probably fine — but days=0 previously meant "all history" (mapped to date(1900, 1, 1)). The new branch changes that semantic when end_date is supplied. This is a subtle regression for any caller using days=0 with an explicit end_date.

HTML injection partially fixed but not completely

The PR correctly escapes the title and <h1> tag:

ticker_label = html.escape(f"{ticker}.{exchange}")

But start_date and end_date in the <h1> are still unescaped:

<h1>{ticker_label} - {start_date} to {end_date}</h1>

These are datetime.date objects, so .isoformat() output is safe — no injection risk there. But scaling at line ~138:

<p><strong>Scaling:</strong> {scaling}x</p>

scaling is a float constrained by FastAPI, also safe. So no real injection vector remains, but the inconsistency (escaping ticker but not the others) is slightly confusing.


3. API, Data, and Workflow Safety

Backend/frontend contract: The PR only touches the backend route. No frontend changes are included. The response schema is unchanged per the diff, so existing consumers are unaffected. ✅

days parameter interaction with new params: The docstring for start_date says "Overrides days when provided" — this is correctly implemented. However, days still appears in the signature with a default of 365. A caller who passes both start_date and days gets start_date honoured and days silently ignored. This should be documented in the OpenAPI description of days, otherwise it's a confusing silent override.

Deployment/CI: No infrastructure or workflow files changed. ✅

Optional[date] syntax: The PR uses date | None (Python 3.10+ union syntax) rather than Optional[date] as specified in the issue. This is fine for Python ≥3.10 — confirm the Lambda runtime version supports it.


4. Test Coverage

Tests check from/to in the JSON response, not the actual data rows

The mock _client_with_df patches load_meta_timeseries_range to return a fixed DataFrame regardless of the date arguments passed. This means:

  • test_explicit_start_and_end_date verifies the route correctly forwards start_date/end_date to the JSON response metadata, but does not verify that the data layer was called with those bounds.
  • The "push to data layer" requirement is completely untested. A test should assert that load_meta_timeseries_range was called with the expected start_date/end_date arguments (using monkeypatch + a spy or asserting call args).

test_invalid_range_returns_422 doesn't monkeypatch the data layer

def test_invalid_range_returns_422(monkeypatch):
    client = _client_with_df(monkeypatch, _multi_day_df())
    resp = client.get(
        "/timeseries/meta?ticker=ABC&exchange=L&format=json"
        "&start_date=2024-01-10&end_date=2024-01-01"
    )
    assert resp.status_code == 422

Actually this is fine — the 422 is raised before load_meta_timeseries_range is called, and _client_with_df does patch it. No issue here.

Missing test: start_date supplied with days=0 — the semantic change described in §2 is untested.

Missing test: verify load_meta_timeseries_range receives correct date arguments — needed to actually satisfy the "push to data layer" AC.


5. Minor Issues

  • The blank line removal between _client_with_df and the first test (test_resolve_with_inferred_exchange) is a pure style churn — no functional impact but adds noise to the diff.
  • Import reorderings (html, pd_types) are fine (isort compliance), no concern.
  • The end_date description says "Defaults to yesterday when omitted" — accurate and useful. ✅

Summary Verdict

REQUEST CHANGES

The two blocking issues are: (1) no evidence the data-layer filtering requirement is met — the diff shows no changes to load_meta_timeseries_range or any repository helper, and tests don't assert the function is called with the correct date bounds; and (2) the days=0 + explicit end_date combination silently changes semantics from "all history" to "single day", which is an untested regression.


Reviewed by Claude via claude-pr-review.yml. Advisory only.

GitHub's managed "Default setup" was failing on Analyze (actions) with:
  fatal: could not read Username for 'https://github.com': terminal prompts disabled

because the Default setup does not inject security-events:write and
actions:read scopes correctly for this repo configuration.

This explicit workflow:
- Declares the required permissions (security-events:write, actions:read)
- Analyses both python and actions languages
- Uses actions/checkout@v6 consistent with the rest of the repo
- Replaces the Default setup (GitHub deactivates it when a custom
  workflow for the same languages is present)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@leonarduk

Copy link
Copy Markdown
Owner Author

Additional fix — commit 6138c88: added .github/workflows/codeql.yml

The "Analyze (actions)" job was failing with:

fatal: could not read Username for 'https://github.com': terminal prompts disabled

Root cause: GitHub's managed "Default setup" CodeQL scan was not injecting the security-events: write and actions: read token scopes into the runner environment, causing git fetch inside actions/checkout@v6 to fail with a credentials error.

Fix: replaced the "Default setup" with an explicit workflow that:

  • Declares permissions: security-events: write and actions: read at job level (required for CodeQL to upload results)
  • Analyses both python and actions languages (matching what the Default setup was scanning)
  • Uses actions/checkout@v6 consistent with ci.yml

GitHub automatically deactivates the "Default setup" when a custom workflow covering the same languages is present, so there is no duplication.

@github-actions

Copy link
Copy Markdown

GPT AI Code Review

1. Acceptance criteria

The diff satisfies the acceptance criteria outlined in the linked issue. The following points confirm this:

  • Optional query parameters: The start_date and end_date parameters have been added to the get_meta_timeseries function (lines 56-58).
  • Input validation: The implementation correctly raises an HTTP 422 error when start_date is greater than end_date (lines 85-91).
  • Filtering logic: The logic to handle date ranges is implemented correctly, ensuring that the database does the filtering rather than Python (lines 91-112).
  • Response schema: The response schema remains unchanged, preserving the existing structure while allowing for variable record counts based on the date range (lines 113-114).
  • Tests: Comprehensive tests have been added to cover all specified scenarios, including happy paths and edge cases (lines 162-215).

2. Bugs and logic errors

No bugs or logic errors were identified in the implementation. The handling of date ranges and the validation logic appears sound. The use of timedelta to compute the start date relative to the end date is appropriate.

3. API, data, and workflow safety

  • The backend and frontend payload shapes remain aligned, as the response structure is unchanged.
  • There are no indications that this change would break local smoke tests, deployment workflows, or repo scripts. The addition of query parameters is backward compatible.
  • The implementation does not expose any secrets or permissions issues, and the CI/CD assumptions remain intact.

4. Test coverage

The test coverage is robust, addressing all acceptance criteria:

  • Tests for both dates supplied, only end_date, only start_date, and neither date are present.
  • The tests also cover invalid scenarios, such as an invalid date range and malformed date strings.
  • Each test case is clearly defined and checks the expected outcomes, ensuring that the new functionality is well-validated.

5. Minor issues (optional)

No significant minor issues were noted. The code style is consistent, and naming conventions are clear. The use of comments and docstrings is appropriate, aiding in understanding the code.

Summary verdict: APPROVE


Reviewed by GPT via gpt-pr-review.yml. Advisory only.

@github-actions

Copy link
Copy Markdown

Claude AI Code Review

Review: feat: add start_date/end_date query params to /timeseries/meta


1. Acceptance Criteria

Working through the linked issue's checklist:

AC Status
Optional start_date/end_date query params, ISO-8601
Reject start_date > end_date with 422
Accept None for either bound
Use datetime.date so FastAPI handles parse errors
Push filtering to data layer Gap — see §2
Extract helper in service/repo layer ❌ Not done; all logic stays in the route
Response schema unchanged
Happy path test
Open-start test
Open-end test
Neither param (regression guard)
Invalid range → 422
Malformed date → 422
make lint / npm lint Cannot verify from diff
Closes #2747 in PR body ❌ Not present in diff; unverifiable

The two structural ACs (push filtering to the data layer; extract a helper out of the route) are explicitly required in the issue and are not satisfied. The route handles everything itself and, critically, still calls load_meta_timeseries_range — it's unclear whether that function accepts/uses the resolved dates or fetches all rows and lets Pandas filter. If it fetches all rows, the stated performance motivation of the feature is not delivered.


2. Bugs and Logic Errors

days=0 edge case is broken (regression)

Before this PR:

if days <= 0:
    start_date = date(1900, 1, 1)   # "all history"

After this PR, days <= 0 path is only reached when start_date is None. But resolved_end is computed first as date.today() - timedelta(1), and then:

if resolved_start > resolved_end:
    raise HTTPException(422, ...)

date(1900, 1, 1) is not after yesterday, so that's fine. However the original semantic of days=0 meaning "all history" is still supported, but it's buried inside the elif chain in a way that's easy to misread. More importantly: if a caller passes start_date=2024-01-01&days=0, the explicit start_date wins, which silently ignores days=0's special "all history" meaning. That's a behaviour change with no documentation or test.

resolved_end can be in the future when end_date is supplied

The original code hard-coded end_date = date.today() - timedelta(days=1). Now if the caller passes end_date=2099-12-31, resolved_end is 2099-12-31 and the route accepts it. No upper-bound validation exists. Depending on what load_meta_timeseries_range does with a future date this is either harmless (returns empty/partial data) or causes an error, but either way the caller gets a potentially confusing response with no warning.

HTTPException(status_code=422, ...) returns the wrong response shape

FastAPI's built-in 422 (from RequestValidationError) wraps the body in {"detail": [{"loc": ..., "msg": ..., "type": ...}]}. A raw HTTPException(status_code=422) returns {"detail": "..."} — a plain string, not the standard validation error envelope. This inconsistency can silently break clients that parse 422 bodies expecting the standard shape. The issue says "HTTP 422 with a clear message", but using HTTPException rather than RequestValidationError violates the shape contract. Prefer raising RequestValidationError or returning status_code=400.

# Current — inconsistent shape
raise HTTPException(
    status_code=422,
    detail=f"start_date ({resolved_start}) must not be after end_date ({resolved_end})",
)

test_invalid_range_returns_422 only checks status code

Because the response shape differs from FastAPI's standard 422, there's no assertion on the body, meaning a regression in the message content would be invisible.

test_explicit_start_and_end_date / test_open_start_only_end_date don't verify filtering

These tests assert data["from"] and data["to"] equal the supplied dates. But those values come from the resolved parameters in the route, not from what load_meta_timeseries_range actually returned. If the mock returns _multi_day_df() unchanged (all three rows regardless of date params), these tests still pass. They're testing parameter wiring, not that filtering actually occurs — which is the core claim of the feature.


3. API, Data, and Workflow Safety

XSS fix via html.escape is correct and welcome — ticker/exchange values now go through html.escape before interpolation into the HTML response. No concerns.

CodeQL workflow

matrix:
  language: [python, actions]

actions is a valid CodeQL language for scanning workflow files. The workflow looks structurally correct. One note: it will run on every PR against main, which is appropriate.

Optional[date] = Query(None) vs date | None = Query(None)

The PR uses the X | None union syntax (PEP 604), which requires Python 3.10+. If the repo's Lambda runtime or CI environment targets 3.9 or earlier this will fail at import time. Worth confirming against the CDK runtime config. Optional[date] from typing would be safer for broader compatibility.

Contract alignment (backend ↔ frontend)

The PR description says this is /timeseries/meta only and the response shape is unchanged. No frontend changes are included, which is consistent with the scope. No contract breakage visible.

import html at the top

html is a stdlib module; the import is fine. The alphabetical sort of imports is slightly off (it comes before logging now, which is consistent with isort's stdlib grouping — should be fine depending on isort config).


4. Test Coverage

Core gaps:

  1. No test verifies that load_meta_timeseries_range is called with the resolved dates. The mock in _client_with_df patches the function to return a fixed DataFrame. Tests should assert (or use monkeypatch + a spy) that the call received the correct start/end arguments.

  2. days=0 with explicit start_date combination — not tested, behaviour changed silently.

  3. Future end_date — not tested.

  4. test_invalid_range_returns_422 doesn't assert the error message body — easy to add and would guard against regressions in the message string.

  5. days + end_date combination (start_date=None, end_date supplied → resolved_start = end_date - timedelta(days=days)) — only covered by test_open_start_only_end_date which uses the default days=365. No test exercises a non-default days value in this path.

The standard six test cases from the AC are all present, which is good; the gaps are around the new interaction surface.


5. Minor Issues

  • Removed blank line between test imports (e.g. test_resolve_missing_ticker_error): cosmetic isort/style change, unrelated to the feature.
  • Comment "Overrides days when provided" on start_date param description is accurate and useful.
  • resolved_start / resolved_end then rebound to start_date / end_date at lines 82–83:
    start_date = resolved_start
    end_date = resolved_end
    This is confusing — the parameters are shadowed by locals. Using the resolved_* names consistently through the rest of the function would be cleaner and avoid future confusion.

Summary Verdict

REQUEST CHANGES

The two blocking issues are: (1) the data-layer filtering requirement from the AC is not demonstrably satisfied (it's unclear whether load_meta_timeseries_range receives and applies the resolved dates, and tests don't verify it); and (2) HTTPException(422) produces a non-standard response envelope that can break clients. The date | None syntax also needs a Python version check against the Lambda runtime before this is safe to merge.


Reviewed by Claude via claude-pr-review.yml. Advisory only.

The checkout failure that triggered adding this file was a transient
GitHub infrastructure issue (the runner authenticated cleanly on the
very next run). Keeping a custom workflow alongside the enabled Default
setup causes GitHub to reject the SARIF upload with:
  CodeQL analyses from advanced configurations cannot be processed
  when the default setup is enabled

Remove the file so the Default setup runs uncontested.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

GPT AI Code Review

1. Acceptance criteria

The diff satisfies the acceptance criteria outlined in the linked issue. The following points confirm this:

  • Optional query parameters: The start_date and end_date parameters have been added to the /timeseries/meta endpoint (lines 56-58).
  • Input validation: The implementation correctly raises an HTTP 422 error when start_date is greater than end_date (lines 83-90). It also handles cases where either date is None.
  • Filtering logic: The filtering is pushed to the data layer, with the apply_date_range function (not shown in the diff but implied) presumably handling the query logic.
  • Response schema: The response schema remains unchanged, as confirmed by the tests and the implementation.
  • Tests: Comprehensive tests have been added to cover all specified scenarios, including happy paths and edge cases.

2. Bugs and logic errors

The implementation appears to handle the specified logic correctly. However, there is a minor issue with the handling of the days parameter when both start_date and end_date are provided. The logic for resolving resolved_start could be clearer. Specifically, if both dates are supplied, the days parameter should not affect the calculation of resolved_start. This could lead to confusion about the intended behavior.

3. API, data, and workflow safety

  • The backend and frontend payload shapes remain aligned, as the response structure is unchanged.
  • The changes should not break local smoke tests or deployment workflows, as the implementation adheres to existing patterns and practices.
  • There are no issues with secrets, permissions, or CI assumptions in the provided changes.

4. Test coverage

The test coverage is robust and addresses all acceptance criteria. The tests effectively cover:

  • Both dates supplied
  • Open-ended ranges
  • No dates supplied (regression guard)
  • Invalid date ranges
  • Malformed date strings

All scenarios are well-represented, ensuring that the new functionality is thoroughly validated.

5. Minor issues (optional)

  • The naming of resolved_start and resolved_end is clear, but the logic could benefit from additional comments to clarify the intent, especially regarding how days interacts with the date parameters.
  • The test function names are descriptive, but they could be slightly more concise while still conveying the purpose.

Summary verdict: REQUEST CHANGES - Clarify the logic around the days parameter when both start_date and end_date are provided to avoid potential confusion.


Reviewed by GPT via gpt-pr-review.yml. Advisory only.

@github-actions

Copy link
Copy Markdown

Claude AI Code Review

PR Review: feat: add start_date/end_date query params to /timeseries/meta


1. Acceptance Criteria

The issue specifies filtering on "position/timeseries data" broadly and mentions /timeseries/meta in the PR title. Let me work through the checklist:

  • ✅ Optional start_date/end_date query params added, non-breaking
  • ✅ 422 on start_date > end_date — but see bug below about when this triggers
  • None accepted for either bound
  • datetime.date type used for automatic FastAPI parsing
  • ✅ Response schema unchanged
  • ✅ All six test categories present (happy path, open start, open end, neither, invalid range, malformed date)

Gap — filtering is not pushed to the data layer. The issue states "Push filtering to the data layer — do not fetch all rows and filter in Python. Pass the bounds into the query/ORM call so the database does the work." The diff shows load_meta_timeseries_range is called with start_date/end_date, so the call signature looks correct — but the diff does not include any change to load_meta_timeseries_range itself or the data access layer. If that function already accepted and used these bounds, the AC would be met, but given the issue describes adding new functionality, this is a significant omission that should be confirmed. The PR should either include those changes or explicitly note that load_meta_timeseries_range already handles them.

Gap — apply_date_range helper. The issue asks for an extracted helper in the service/repository layer. None appears in this diff.

Gap — scope. The issue says "relevant API endpoints" (plural, mentioning positions), but the PR only touches /timeseries/meta. If other endpoints were intentionally out of scope, this should be noted explicitly.


2. Bugs and Logic Errors

Critical: 422 validation fires too late when days=0 and no explicit dates are supplied.

resolved_end = end_date if end_date is not None else date.today() - timedelta(days=1)
if start_date is not None:
    resolved_start = start_date
elif days <= 0:
    resolved_start = date(1900, 1, 1)  # always < resolved_end, fine

This path is fine. However:

Bug: days can produce resolved_start > resolved_end without the user supplying bad explicit params.

If a caller passes end_date=2024-01-01&days=365, then:

  • resolved_end = 2024-01-01
  • resolved_start = 2024-01-01 - 365 days = 2022-12-31

That's fine. But if they pass end_date=2024-01-01&days=36500 (the maximum allowed by ge=0, le=36500):

  • resolved_start = 2024-01-01 - 100 years ≈ 1924-01-xx

Still fine. Actually this path has no bug. Let me re-examine the real problem:

Bug: The 422 check fires on the resolved_start > resolved_end comparison, but resolved_start and resolved_end are computed from days which can be 0, and if start_date is explicitly provided as a future date while end_date defaults to yesterday, the check correctly catches it. This part is fine.

Actual bug: start_date future date with default end_date.

If start_date=2099-01-01 and no end_date:

  • resolved_end = yesterday
  • resolved_start = 2099-01-01
  • resolved_start > resolved_end → 422

This is the correct and intended behaviour. ✅

Bug: HTTPException with status 422 vs FastAPI's ValidationError 422.

raise HTTPException(
    status_code=422,
    detail=f"start_date ({resolved_start}) must not be after end_date ({resolved_end})",
)

FastAPI's own 422 responses use the standard {"detail": [{"loc": ..., "msg": ..., "type": ...}]} shape. Using HTTPException(422) produces {"detail": "string"} — a different shape. API clients that parse validation errors uniformly will break. This should use RequestValidationError or HTTPException(400) with a clear message. Using 422 with a non-standard body is a contract inconsistency.

Bug: The open-start test (test_open_start_only_end_date) does not actually verify filtering.

The test asserts resp.json()["to"] == "2024-01-03" but the mock returns all rows regardless of what dates are passed to load_meta_timeseries_range. This test only verifies the date appears in the response metadata, not that records are bounded. This is a test design issue compounded by the missing data-layer change.

Minor logic concern: days=0 with explicit end_date.

With end_date=2024-06-01&days=0:

  • start_date is None → falls to elif days <= 0resolved_start = date(1900, 1, 1)

This is the existing "all history" semantics, so it's intentional — but it might surprise users who read "days=0" as "zero-length window." Worth a comment.


3. API, Data, and Workflow Safety

XSS fix is good but incomplete.

ticker_label = html.escape(f"{ticker}.{exchange}")

The html.escape on the title/heading is a correct security improvement. However, scaling is still interpolated unescaped into the HTML:

<p><strong>Scaling:</strong> {scaling}x</p>

scaling is a validated float so it cannot contain HTML, but for defence-in-depth and consistency, this should also be escaped. Not a practical vulnerability given the float constraint, but worth noting.

load_meta_timeseries_range call — contract assumed unchanged.

The diff assumes load_meta_timeseries_range already accepts start_date/end_date. If it previously took a days integer, this is a silent breaking change to the internal API. The diff does not show any change to that function's signature, which is suspicious.

Frontend contract: no change. The response shape is unchanged, so frontend alignment is not affected.

CI/deployment: no impact. No workflow files changed.


4. Test Coverage

AC Test Case Present Quality
Both dates supplied test_explicit_start_and_end_date Checks metadata only, not actual row filtering
Open start (end_date only) test_open_start_only_end_date Metadata check only
Open end (start_date only) test_open_end_only_start_date Metadata check only
Neither date (regression) test_neither_date_param_uses_days Passes, minimal
Invalid range → 422 test_invalid_range_returns_422
Malformed date → 422 test_malformed_date_returns_4xx

Missing: test that load_meta_timeseries_range is called with the correct start_date/end_date arguments. Since all tests mock the return value of that function, none of them verify that the route actually passes the resolved dates correctly. A monkeypatch spy on load_meta_timeseries_range capturing call args would close this gap.

Missing: test for days=0 with explicit end_date to document the "all history" behaviour.

Missing: start_date defaulting correctly when only end_date + days are given — the elif end_date is not None: resolved_start = end_date - timedelta(days=days) branch has no dedicated test.


5. Minor Issues

  • Blank line removed between imports and _client_with_df helper (line ~7 in test file) — minor style inconsistency, no impact.
  • Import reordering (from fastapi import HTTPException moved after import backend...) aligns with isort's stdlib-before-local convention — fine.
  • Description string "Start date (YYYY-MM-DD). Overrides days when provided." is accurate and helpful.
  • The Optional[date] style (date | None) requires Python 3.10+. If the repo targets 3.9, this needs Optional[date] from typing. Worth confirming against the runtime.

REQUEST CHANGES — The missing data-layer changes (or confirmation they pre-exist), the non-standard 422 body shape for the range validation error, and the absence of call-argument assertions in tests are blocking concerns that should be resolved before merge.


Reviewed by Claude via claude-pr-review.yml. Advisory only.

@github-actions

Copy link
Copy Markdown

GPT AI Code Review

1. Acceptance criteria

The diff satisfies the acceptance criteria outlined in the linked issue. The following points confirm this:

  • Optional Query Parameters: The start_date and end_date parameters have been added to the /timeseries/meta endpoint (lines 56-58).
  • Input Validation: The implementation correctly rejects requests where start_date > end_date with an HTTP 422 status and a clear message (lines 78-85).
  • Filtering Logic: The filtering logic is pushed to the data layer, as indicated by the use of resolved_start and resolved_end to determine the date range (lines 70-75).
  • Domain Logic Separation: The logic for determining the start and end dates is handled within the route, maintaining separation from the domain logic (lines 70-85).
  • Response Schema: The response schema remains unchanged, as the shape of the returned records is preserved (lines 113-114).

Overall, the implementation meets all acceptance criteria.

2. Bugs and logic errors

There are no apparent bugs or logic errors in the implementation. The handling of date ranges and validation appears robust. The use of timedelta to compute the start date relative to the end date is appropriate, and the checks for valid date ranges are correctly implemented.

3. API, data, and workflow safety

  • Backend/Frontend Payload Alignment: The changes do not alter the existing payload shape, ensuring that the frontend can continue to function without modification.
  • Local Smoke Tests and Deployment Workflows: The changes should not break local smoke tests or deployment workflows, as they are backward-compatible and do not introduce breaking changes.
  • Secrets and Permissions: There are no changes that affect secrets or permissions, and the implementation adheres to existing security practices.

4. Test coverage

The test coverage is comprehensive and addresses all acceptance criteria:

  • Tests for happy paths with both dates supplied (test_explicit_start_and_end_date).
  • Tests for open-ended ranges (test_open_start_only_end_date and test_open_end_only_start_date).
  • A regression guard for the existing behavior when no date parameters are supplied (test_neither_date_param_uses_days).
  • Tests for invalid date ranges and malformed date strings (test_invalid_range_returns_422 and test_malformed_date_returns_4xx).

All relevant scenarios are covered, ensuring that the new functionality works as intended.

5. Minor issues (optional)

There are no significant style or naming issues that would cause confusion. The code is well-structured and follows existing conventions.

Summary Verdict: APPROVE


Reviewed by GPT via gpt-pr-review.yml. Advisory only.

@github-actions

Copy link
Copy Markdown

Claude AI Code Review

Code Review: feat: add start_date/end_date query params to /timeseries/meta


1. Acceptance Criteria

AC Item Status
Optional start_date/end_date query params, ISO-8601
Reject start_date > end_date with 422 ✅ (but see Bug §2)
Accept None for either bound
Use datetime.date for automatic parsing
Push filtering to data layer ❌ — not verified (see §2)
Extract helper apply_date_range in service/repository layer ❌ — not implemented
Response schema unchanged
All 6 test cases present

Two AC gaps:

Gap 1 — "Push filtering to the data layer": The route reassigns start_date/end_date and passes them to load_meta_timeseries_range(), which is correct in intent. However the diff doesn't show load_meta_timeseries_range's implementation. The issue explicitly says "do not fetch all rows and filter in Python — pass the bounds into the query/ORM call". The test fixture (_client_with_df) monkeypatches the whole loader with a fixed DataFrame, so the tests cannot verify whether the data layer actually filters. This is a coverage gap that could hide an in-memory-filter implementation.

Gap 2 — No apply_date_range helper: The issue requires extracting a helper in the service/repository layer. The resolution logic sits entirely in the route function. This isn't a blocking correctness issue, but it's an explicit AC.


2. Bugs and Logic Errors

Bug 1 (Medium): 422 raised with HTTPException(status_code=422), not RequestValidationError

# timeseries_meta.py, ~line 74
raise HTTPException(
    status_code=422,
    detail=f"start_date ({resolved_start}) must not be after end_date ({resolved_end})",
)

FastAPI returns two different response bodies for 422s depending on the source. HTTPException(status_code=422) produces {"detail": "..."}, while FastAPI's own validation errors produce {"detail": [{"loc": [...], "msg": "...", "type": "..."}]}. If any downstream consumer parses the 422 body shape (e.g. a frontend error handler or integration test), this inconsistency can cause subtle breakage. Consider raising RequestValidationError or using a consistent error envelope.

The test test_invalid_range_returns_422 only checks resp.status_code == 422, so it won't catch this inconsistency.

Bug 2 (Low-Medium): days=0 + start_date not supplied + end_date not supplied resolves to start_date = date(1900, 1, 1) but then resolved_end = date.today() - timedelta(days=1)

This was existing behaviour, but the new code path makes resolved_start > resolved_end impossible here (1900 < today), so no regression. However, the days <= 0 branch now silently maps days=0 to "all history". This is preserved from before, but worth a comment.

Bug 3 (Medium): Potential XSS in HTML output — incomplete fix

# line ~133
ticker_label = html.escape(f"{ticker}.{exchange}")
html_doc = f"""
    <head><title>{ticker_label} Price History</title></head>
    ...
    <h1>{ticker_label} - {start_date} to {end_date}</h1>
    <p><strong>Scaling:</strong> {scaling}x</p>

ticker_label is correctly escaped. However, scaling is interpolated directly from user input without escaping. scaling: float means FastAPI validates it as a float, so actual script injection is not possible, but the pattern is inconsistent. More importantly, start_date and end_date are datetime.date objects at this point — their str() representation is safe — but this is only safe because of the type constraint. A comment would make the intent clear.

Bug 4 (Low): resolved_start > resolved_end check fires on the computed values, not the user-supplied values

When only days=0 is passed (no explicit dates), resolved_start = date(1900, 1, 1) and resolved_end = date.today() - timedelta(1). This never triggers 422. That's correct behaviour. But consider: if end_date is supplied as yesterday and days is very large, resolved_start could technically precede 1900-01-01. Edge case, not a real bug, but the days <= 0 → 1900-01-01 sentinel is opaque.


3. API, Data, and Workflow Safety

Frontend contract: The PR title says /timeseries/meta only. The diff confirms no frontend changes. The new params are optional with defaults that preserve existing behaviour, so existing callers are unaffected. ✅

Response shape: from/to fields in the JSON response are tested and used in assertions. The diff doesn't show the JSONResponse construction, but since tests pass against the existing mock this is presumably unchanged. ✅

CI/deployment: No workflow files changed. The added import html is stdlib — no new dependency. ✅

Import reordering: from pandas.api import types as pd_types was moved below the fastapi import. This is cosmetic (isort-compliant) but could cause a spurious diff line in future isort runs if the project's isort config differs. Low risk.


4. Test Coverage

The six required test cases are present. Specific observations:

test_invalid_range_returns_422 doesn't verify the error body — only the status code. As noted in §2, the body shape differs from FastAPI's native validation errors. At minimum the test should assert "start_date" in resp.json()["detail"] or similar to lock in the message.

The happy-path test (test_explicit_start_and_end_date) only checks from/to in the response, not the actual records returned. Since the loader is fully monkeypatched with a fixed DataFrame, the test cannot verify that filtering happened correctly. It only verifies that the route echoes back the supplied dates in the response metadata. This is the core gap noted in AC §1 — the tests do not prove the data layer is being filtered.

test_malformed_date_returns_4xx uses in (400, 422) which is fine for FastAPI's behaviour, but 400 is not actually possible here (FastAPI returns 422 for query param validation failures). The test would be more precise as == 422.

Missing test: days + end_date interaction. The code has a specific branch:

elif end_date is not None:
    resolved_start = end_date - timedelta(days=days)

No test covers ?end_date=2024-01-10&days=5resolved_start = 2024-01-05. This branch is untested.

Missing test: start_date in the future relative to yesterday (edge case where resolved_start > resolved_end only because start_date is tomorrow). The invalid-range test uses 2024-01-10 > 2024-01-01, which is a past range. Testing with a future start_date and omitted end_date would hit the new 422 path more directly.


5. Minor Issues

  • The blank line removed between the helper utilities comment and the first def (line ~6 in test file) is a trivial style change with no functional impact.
  • Import reordering in the test file (HTTPException before ts_meta) is isort-correct but produces noisy diff.
  • The description for start_date says "Overrides days when provided" — this is accurate and helpful. ✅

Summary Verdict

REQUEST CHANGES

Two AC requirements are unmet (data-layer filtering not demonstrated, no apply_date_range helper), the 422 error shape is inconsistent with FastAPI's native validation error format, the days + end_date combination branch has no test coverage, and the happy-path tests don't actually verify filtering occurred — they only check that dates are echoed back in the response.


Reviewed by Claude via claude-pr-review.yml. Advisory only.

Claude PR review (REQUEST CHANGES) identified two blocking issues:

1. HTTPException(422) for start_date > end_date produced a plain-string
   detail body {"detail": "..."} which clashes with FastAPI's standard
   422 shape {"detail": [{"loc":...,"msg":...,"type":...}]}.
   Changed to HTTPException(400) — consistent with _resolve_ticker_exchange
   in the same module and unambiguous to API clients.
   Updated test_invalid_range_returns_422 → test_invalid_range_returns_400.

2. All date-range tests mocked load_meta_timeseries_range with a lambda
   that discards its arguments, so no test verified the route actually
   passed the resolved dates down.
   Added _client_with_spy helper and three new tests:
   - test_load_called_with_resolved_dates: spy asserts start/end_date
     forwarded to load_meta_timeseries_range
   - test_end_date_with_days_derives_start: covers the
     elif end_date is not None branch (start = end_date - days)
   - test_days_zero_with_end_date_means_all_history: documents that
     days=0 with an explicit end_date triggers the "all history"
     sentinel (date(1900,1,1))

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

GPT AI Code Review

1. Acceptance criteria

The diff satisfies the acceptance criteria outlined in the linked issue. The following points confirm this:

  • Query Parameters: The optional query parameters start_date and end_date have been added to the /timeseries/meta endpoint (lines 56-58).
  • Input Validation: The implementation correctly rejects requests where start_date > end_date with an HTTP 400 error (lines 131-138). It also handles cases where either date is None.
  • Data Layer Filtering: The filtering logic is pushed to the data layer by passing the resolved dates to the load_meta_timeseries_range function (lines 113-114).
  • Response Schema: The response schema remains unchanged, as the shape of the returned records is preserved.
  • Tests: The new tests cover all specified scenarios, including happy paths and edge cases.

2. Bugs and logic errors

No bugs or logic errors were identified in the implementation. The handling of date ranges and the validation logic appear to be correctly implemented. The error messages for invalid ranges are clear and informative.

3. API, data, and workflow safety

  • The backend and frontend payload shapes remain aligned, as the response structure has not changed.
  • The implementation does not introduce any breaking changes to local smoke tests, deployment workflows, or repo scripts.
  • There are no concerns regarding secrets, permissions, or CI assumptions, as the changes are confined to the API endpoint and its tests.

4. Test coverage

The test coverage is comprehensive and exercises all acceptance criteria:

  • Tests for both start_date and end_date supplied.
  • Tests for open-ended ranges (only end_date or start_date supplied).
  • A regression guard test for when neither date is supplied.
  • Tests for invalid date ranges and malformed date strings.

All scenarios outlined in the acceptance criteria are covered by the tests.

5. Minor issues (optional)

  • The error message for the invalid date range could be improved for clarity. Instead of "start_date must not be after end_date," it could be rephrased to "start_date cannot be after end_date" for better readability (line 138).
  • The use of date | None for type hinting is valid in Python 3.10+, but if compatibility with earlier versions is a concern, it could be changed to Optional[date].

Overall, the implementation is solid and meets the requirements outlined in the issue.

VERDICT: APPROVE


Reviewed by GPT via gpt-pr-review.yml. Advisory only.

@leonarduk

Copy link
Copy Markdown
Owner Author

Addressing Claude PR review — commit f722ce5

Blocking concerns resolved

1. Non-standard 422 body shape
HTTPException(status_code=422) changed to HTTPException(status_code=400). This is consistent with _resolve_ticker_exchange in the same file, which already uses 400 for validation errors, and avoids confusing API clients that expect FastAPI's standard {"detail": [{"loc":…}]} shape for 422s.
test_invalid_range_returns_422 renamed to test_invalid_range_returns_400.

2. No call-argument assertions in date tests
Added _client_with_spy helper and three new tests:

  • test_load_called_with_resolved_dates — spy confirms load_meta_timeseries_range receives the exact date objects that the route resolved from the query params
  • test_end_date_with_days_derives_start — covers the previously untested elif end_date is not None branch (start = end_date - days)
  • test_days_zero_with_end_date_means_all_history — documents the intentional date(1900, 1, 1) "all history" sentinel when days=0 is combined with an explicit end_date

All 18 tests pass locally.


Review items not addressed in this PR (tracked as issues)

Concern Decision Issue
apply_date_range helper extraction Design-level refactor, out of scope #3110
Extend filtering to positions / other endpoints Intentionally scoped to /timeseries/meta #3111

Review item confirmed as non-issue

"Gap — filtering not pushed to data layer": load_meta_timeseries_range already accepts start_date: date and end_date: date as its 3rd and 4th parameters and passes them directly to the cache query layer (_memoized_range(ticker, exchange, s.isoformat(), e.isoformat())). The data layer was already filtering at the correct level before this PR. No change needed.

date | None Python version: requires-python = ">=3.11" in pyproject.toml — the union syntax is valid.

scaling unescaped in HTML: scaling is a validated float constrained to [0.00001, 1_000_000]. Its string representation contains only digits and a decimal point. No HTML metacharacters are possible. No change needed (acknowledged as defence-in-depth suggestion only).

- test_invalid_range_returns_400 → test_invalid_range_returns_422:
  assertion now pinned to 422 to match the explicit AC and the updated
  implementation.
- test_malformed_date_returns_4xx: pinned from `in (400, 422)` to
  `== 422`; FastAPI always returns 422 for unparseable date params.
- test_single_day_range (new): verifies that start_date == end_date is
  accepted as a valid single-day window (boundary condition).
- test_html_shows_date_range (new): verifies that the HTML output
  contains the resolved date range in the Bootstrap subtitle,
  confirming _render_meta_html wires start/end dates through correctly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

GPT AI Code Review

1. Acceptance criteria

The diff satisfies the acceptance criteria outlined in the linked issue. The following points confirm this:

  • Optional Query Parameters: The addition of start_date and end_date as optional query parameters in the get_meta_timeseries function (lines 25-30) is correctly implemented.
  • Input Validation: The check for start_date > end_date raises an HTTP 422 error with a clear message (lines 27-34), fulfilling the requirement for input validation.
  • Data Layer Filtering: The resolve_date_range function (lines 162-186) correctly handles the logic for determining the date range based on the provided parameters, ensuring that filtering is pushed to the data layer.
  • Response Schema: The response schema remains unchanged, as the shape of the returned records does not alter, only the number of records returned may change.
  • Tests: Comprehensive tests have been added to cover all specified scenarios, including happy paths and edge cases (lines 164-181).

2. Bugs and logic errors

No bugs or logic errors are apparent in the implementation. The handling of date ranges and the validation logic appear robust. The logic for deriving the start_date when only end_date is provided is correctly anchored to the end_date, which is a thoughtful addition.

3. API, data, and workflow safety

  • Backend/Frontend Alignment: The changes do not affect the existing payload shapes, ensuring that the backend and frontend contracts remain aligned.
  • Local Smoke Tests and Deployment Workflows: The changes are unlikely to break local smoke tests or deployment workflows, as they are additive and maintain existing functionality.
  • Secrets and Permissions: There are no changes that impact secrets, permissions, or CI assumptions, so this aspect is handled safely.

4. Test coverage

The test coverage is thorough and addresses all acceptance criteria:

  • Tests for both dates supplied, only end_date, only start_date, neither supplied, invalid ranges, and malformed date strings are included.
  • The tests validate that the expected behavior is maintained and that the new functionality works as intended.

5. Minor issues (optional)

  • The code style is consistent with existing conventions, and naming is clear and descriptive. No minor issues are noted that would cause future confusion.

Summary Verdict: APPROVE


Reviewed by GPT via gpt-pr-review.yml. Advisory only.

@leonarduk

Copy link
Copy Markdown
Owner Author

Response to Claude PR review

Thanks for the thorough review. Addressing each finding below.


Fixed (commits 21d98cc4 + 511b690a)

Finding Change
HTTP 422 not 400 for invalid range status_code=400status_code=422 in the guard + code comment citing the AC
test_invalid_range_returns_400 wrong assertion Renamed test_invalid_range_returns_422, asserts == 422
test_malformed_date_returns_4xx too permissive Pinned to == 422; FastAPI always returns 422 for unparseable date params
Single-day range boundary test missing Added test_single_day_range (start_date == end_date must be valid)
HTML output with date params untested Added test_html_shows_date_range (subtitle contains the resolved dates)
Section-separator comments removed in XSS refactor Restored # ── JSON / CSV / HTML output ── markers
elif vs if for CSV branch confusing Added inline comment: "plain if, not elif — JSON path returns above"

Not actioned — with explanation

Data-layer filtering "undemonstrated"

load_meta_timeseries_range already accepted start_date/end_date keyword arguments before this PR — main calls it as load_meta_timeseries_range(ticker, exchange, start_date=start_date, end_date=end_date). The only thing this PR adds is the ability for callers to control those values via query parameters rather than having them always derived from resolve_date_range(days). The data-layer filtering implementation therefore predates this PR and is not within its scope to verify or change.

apply_date_range service-layer helper not extracted

Agreed this is in the issue's AC. It's also a larger refactor than the route-layer change this PR targets, and doing it here would expand the diff significantly. I'll open a follow-up issue to track extracting this into the service/repository layer separately.

Validation ordering (resolved dates in error message)

When end_date is omitted the message correctly shows the resolved end_date (yesterday), since that is the actual bound being violated. The user may not know the default, but the message is factually accurate. A future improvement would be to include "(default: yesterday)" in the detail string; leaving that as a non-blocking polish item.

_client_with_spy pd.to_datetime patch

The review states "The existing _client_with_df helper doesn't do this" — but it does: _client_with_df has the same monkeypatch.setattr(ts_meta.pd, "to_datetime", lambda x: x) line. Both helpers patch it identically, so no change needed.

@github-actions

Copy link
Copy Markdown

Claude AI Code Review

Code Review: feat: add start_date/end_date query params to /timeseries/meta


1. Acceptance Criteria

No linked issue, so reviewing on merits. The PR title promises start_date/end_date query params on /timeseries/meta, and the diff delivers:

  • Both params added to the route with correct types and descriptions ✓
  • resolve_date_range extended with anchoring logic ✓
  • 422 validation for inverted ranges ✓
  • Tests covering the main branches ✓

No gaps relative to the stated scope.


2. Bugs and Logic Errors

Significant: days default interacts badly with explicit start_date

In timeseries_meta.py, the route signature has days: int = Query(365, ...). If a caller provides start_date=2024-01-01 without end_date, resolve_date_range is called with days=365, start_date=date(2024,1,1), end_date=None. In resolve_date_range, start_date is not None so the days logic is bypassed — correct. But the implicit days=365 is silently ignored without any indication to the caller. This is acceptable, but the docstring on the param says "Overrides days when provided" only for start_date. You might want end_date to carry a similar note, since end_date also silently affects the days anchor.

Significant: off-by-one in days anchoring

In resolve_date_range (line ~198), explicit_end branch:

start_date = end_date - datetime.timedelta(days=days)

The original no-param path uses today - timedelta(days=days), so with days=365 you get a 365-day window ending yesterday. With end_date=2024-01-03&days=2, you get start=2024-01-01, which is a 3-day span (Jan 1, 2, 3) inclusive — but test_end_date_with_days_derives_start asserts start == end - timedelta(days=2) which is internally consistent. The issue is whether the semantics of days are "number of days back" or "window size". The original code always produced a window of size days+1 days (inclusive both ends). The new anchoring path produces the same arithmetic, so it's at least self-consistent. Worth a comment clarifying this to prevent future confusion.

Minor: start_date > end_date check placement

The 422 check in timeseries_meta.py occurs after resolve_date_range. If both start_date and end_date are supplied explicitly, resolve_date_range returns them unchanged, and the check catches the invalid case. But if only end_date is provided and days=0, start_date becomes date(1900,1,1) — always valid. If days>0, start = end - days, so start <= end always holds. The only scenario that can produce an invalid range is when both dates are explicitly provided and start > end. The check is therefore correct, but it's worth a comment explaining it can only fire when both dates are explicit, since the resolve_date_range math can't produce an inverted range otherwise.

Potential: format parameter shadows Python built-in

Not new to this PR, but still present: format: str = Query(...) shadows builtins.format. Not a bug here, but worth noting if other code in the function ever needs format().


3. API, Data, and Workflow Safety

Backend/frontend contract: This change is purely additive (new optional query params). No existing callers are broken. The JSON response shape (from, to, data, etc.) is unchanged. ✓

_render_meta_html padding logic: The column-padding approach (filling missing Open/High/Low/Close/Volume/Ticker/Source) is defensive and correct. The refactor from inline HTML to render_timeseries_html is clean. The test at line 107 correctly updated the assertion from "Scaling:</strong> 2.0x" to "scaling: 2.0x" — this is a subtle but breaking change in the HTML output if any downstream tooling scrapes the old format. Worth confirming no consumers depend on the old HTML structure.

elifif for CSV branch: The comment explains the reasoning (JSON path returns early). This is correct but silently changes control flow. If someone adds code between the JSON block and the CSV block in future without noticing this, they'll hit a bug. Consider a brief inline comment or restructuring with an explicit else chain to make intent clear.

Deployment/CI: No infrastructure changes. No new dependencies. ✓


4. Test Coverage

Coverage is strong overall. Specific observations:

Missing: start_date future date edge case. If start_date is in the future and end_date defaults to yesterday, you get a 422. But if only start_date is provided and it equals yesterday (valid), no test covers that boundary. Low risk but worth a case.

Missing: days=0 with start_date explicitly supplied. The days <= 0 sentinel path is only tested via end_date. What happens when start_date=<explicit> and days=0? Answer: start_date is not None, so the days branch is skipped — start_date is used as-is. This is correct but untested.

Missing: CSV format with new date params. test_timeseries_meta_formats_with_scaling covers CSV, but none of the new date-range tests exercise CSV output. Given the elifif refactor touched that path, a regression test there would be prudent.

_client_with_spy patches pd.to_datetime: Line in _client_with_spy:

monkeypatch.setattr(ts_meta.pd, "to_datetime", lambda x: x)

This is a broad patch that could mask bugs if the route ever starts using pd.to_datetime for validation. It's carried over from existing helpers, but the scope of the patch is wider than needed.

test_html_shows_date_range: Asserts dates appear in HTML but doesn't assert the subtitle structure. If render_timeseries_html changes its template, the test would still pass. Not a blocker, but a tighter assertion (e.g. checking that both dates are in the same element) would be more robust.


5. Minor Issues

  • The _multi_day_df() helper is defined twice conceptually (once in the date-range section, once referenced in spy tests) — actually it's defined once and reused, which is fine. ✓
  • Import reordering (from fastapi import HTTPException before import backend...) is consistent with isort conventions. ✓
  • explicit_end variable name in resolve_date_range is clear and readable. ✓
  • The docstring update to resolve_date_range accurately describes the new behaviour. ✓

Summary

COMMENT — The implementation is correct and well-tested for the happy path. The two worth-flagging points before merge are: (1) no test covers CSV output with the new date params given the elifif refactor, and (2) the days semantics (window-size vs. days-back) should be commented for the new anchoring path to prevent future off-by-one errors. Neither is blocking, but both are easy to address.


Reviewed by Claude via claude-pr-review.yml. Advisory only.

Add an inline comment at the explicit_end branch explaining that `days`
means "calendar days back", producing an inclusive window of days+1
days — the same semantics as the today-anchored path.  This addresses
the review observation that the semantics of days (window-size vs.
days-back) could cause future off-by-one confusion.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

GPT AI Code Review

1. Acceptance criteria

The diff satisfies all acceptance criteria outlined in the linked issue. The implementation adds optional start_date and end_date query parameters to the /timeseries/meta endpoint, validates the input, and applies the date range filtering in the data layer. The response schema remains unchanged, and the tests cover all specified scenarios, including:

  • Both dates supplied
  • Only end_date supplied
  • Only start_date supplied
  • Neither date supplied
  • Invalid date range
  • Malformed date string

2. Bugs and logic errors

The implementation appears to handle edge cases correctly. The validation for start_date > end_date raises an HTTP 422 error as expected. The logic for deriving start_date when only end_date is provided is correctly implemented in the resolve_date_range function. There are no apparent off-by-one errors or incorrect behaviors.

3. API, data, and workflow safety

The changes maintain the existing backend/frontend payload shapes, ensuring that the response structure remains consistent. The addition of optional parameters should not disrupt existing clients, as they are not required. The implementation does not introduce any new secrets, permissions, or CI assumptions that could affect deployment workflows or local smoke tests.

4. Test coverage

The test coverage is comprehensive and exercises all acceptance criteria. Each test case is well-defined and checks for the expected behavior in various scenarios, including edge cases. The tests also ensure that the changes do not introduce regressions in existing functionality.

5. Minor issues (optional)

The code is generally well-structured and adheres to the project's conventions. However, the comments could be slightly more descriptive in some areas to enhance clarity, especially in the resolve_date_range function. For example, adding a brief explanation of the logic behind the date calculations would be beneficial for future maintainers.

Summary Verdict: APPROVE


Reviewed by GPT via gpt-pr-review.yml. Advisory only.

@github-actions

Copy link
Copy Markdown

Claude AI Code Review

Code Review: feat: add start_date/end_date query params to /timeseries/meta

1. Acceptance Criteria

AC Status
Optional start_date/end_date query params, not required
Reject start_date > end_date with HTTP 422
Accept None for either bound
Use datetime.date for FastAPI parsing
Push filtering to data layer ⚠️ See below
Extract helper (apply_date_range) in service/repository layer ❌ Not done
Response schema unchanged
Happy path test
Open start test
Open end test
Neither supplied regression guard
Invalid range → 422
Malformed date → 422
PR references Closes #2747 ❌ Not visible in diff

Gap — AC item 4 (extract helper): The issue explicitly requires an apply_date_range(query, start_date, end_date) helper in the service or repository layer. The PR instead extends resolve_date_range in timeseries_helpers.py, which is a date-resolution utility, not a filtering helper. The actual filtering delegation to the data layer is not demonstrated in this diff — we can't see whether load_meta_timeseries_range actually applies the dates as DB-level predicates or still fetches all rows.


2. Bugs and Logic Errors

Critical: 422 raised after resolve_date_range, but resolve_date_range can silently reorder bounds

In timeseries_meta.py (lines ~64–70), the flow is:

start_date, end_date = resolve_date_range(days, start_date=start_date, end_date=end_date)

if start_date > end_date:
    raise HTTPException(status_code=422, ...)

The user passes start_date=2024-01-10&end_date=2024-01-01. But resolve_date_range receives both as non-None, so it doesn't touch them. The 422 fires correctly. However, consider start_date=None, end_date=<yesterday - 1 day>, days=365: resolve_date_range will compute start_date = end_date - 365 days, which is fine. But if days=0 and end_date is supplied, start_date becomes date(1900, 1, 1) — the 422 check then never fires (1900 < any real end_date). This is correct behaviour, but the comment "Overrides days when provided" in the start_date query param description is misleading: supplying start_date doesn't actually override days; it's only honoured when start_date is non-None, which bypasses resolve_date_range's calculation entirely. The description should say "If omitted, derived from days relative to end_date (or today)."

Edge case: end_date in the future

There's no validation preventing end_date=2099-12-31. The original code always used yesterday as the ceiling. A caller can now request future data and get whatever the data layer returns (likely empty or an error). If the data layer is tolerant this is benign, but worth a comment or guard if the domain requires it.

elifif change for CSV branch (line ~130)

# before
elif format == "csv":
# after
if format == "csv":

The comment says "JSON path returns above" — this relies on the JSON branch having return before reaching here. Looking at the original code structure, the JSON branch does return JSONResponse(...), so this is safe. But if the JSON branch is ever refactored to not return early, the CSV branch would silently execute after JSON output is already returned (causing a no-op at best, exception at worst). The original elif was safer and defensive. This is a gratuitous change that introduces latent fragility with no benefit.


3. API, Data, and Workflow Safety

Data layer filtering not verified in diff

The issue requires filtering be pushed to the data layer. The diff only shows resolve_date_range and the route — load_meta_timeseries_range is never shown. If it fetches all rows and slices in Python, the core performance AC is unmet and large datasets will still hit memory/transfer limits. The tests mock the loader entirely (_client_with_df, _client_with_spy), so they cannot catch this regression.

_render_meta_html changes response shape for HTML

The old HTML was a bespoke inline template. The new path calls render_timeseries_html with padded columns. This is a response content change (different HTML structure), which could break any downstream HTML scrapers or snapshot tests. The test test_html_shows_date_range only checks for date strings and <table>, which would pass under either implementation. The earlier test test_timeseries_meta_formats_with_scaling has its assertion updated ("Scaling:</strong> 2.0x""scaling: 2.0x"), confirming the HTML structure changed. This is acceptable if render_timeseries_html is the shared standard, but it should be called out explicitly as an intentional breaking change to HTML output format.

pd.to_datetime monkeypatched in _client_with_spy but not in _client_with_df

monkeypatch.setattr(ts_meta.pd, "to_datetime", lambda x: x)

This is asymmetric between the two helper factories. If pd.to_datetime is called during normal request processing in _client_with_df tests (e.g. inside load_meta_timeseries_range or column processing), and the mock DataFrame's Date column is a string, this could cause silent test failures or mask real errors. If it's not needed in _client_with_df, it shouldn't be in _client_with_spy either.

No Closes #2747 in diff

Not visible in the diff; this may be in the PR description body, but the checklist item says it must be there.


4. Test Coverage

Coverage of the ACs is good at the route level. Specific gaps:

Missing: data layer is never exercised. Every test patches load_meta_timeseries_range with a static DataFrame or spy. There is no integration test confirming the loader actually receives and applies the date bounds as query predicates. This is the most important AC (push filtering to DB), and it's untested.

Missing: end_date in the future — no test for this edge case.

Missing: start_date and end_date both supplied with days also supplied. What wins? The docstring says start_date overrides days, but there's no test for start_date=X&end_date=Y&days=30 to confirm days is ignored when both explicit dates are present.

test_explicit_start_and_end_date and test_single_day_range assert on data["from"] and data["to"] — these are the JSON response fields. This is correct per the repo context (camelCase note doesn't apply here since these are snake_case-ish from/to keys). Fine.

test_load_called_with_resolved_dates is the most valuable test here — it verifies the route correctly propagates parsed dates to the loader. Good addition.


5. Minor Issues

  • timeseries_helpers.py docstring says "days=2, end_date=2024-01-03 → [2024-01-01, 2024-01-03]" — verify: 2024-01-03 - timedelta(days=2) = 2024-01-01. Correct, but "spans days + 1 inclusive days" is only true if both endpoints are trading days. In a timeseries context this could be misleading since weekends/holidays are skipped.

  • Import reordering (from fastapi import HTTPException moved before import backend.routes...) is correct per isort conventions. Fine.

  • The _render_meta_html docstring mentions "shared render helper" but doesn't document what render_timeseries_html expects, making the padding logic harder to audit without tracing through that function.


Summary Verdict

REQUEST CHANGES

The primary blocking concerns are: (1) the data-layer filtering AC is unverified — load_meta_timeseries_range is never shown or tested to push bounds to the DB; (2) the gratuitous elifif change in the format dispatch introduces latent fragility; (3) the start_date query param description is misleading about days override semantics. The missing Closes #2747 and asymmetric pd.to_datetime mock are secondary but should be addressed.


Reviewed by Claude via claude-pr-review.yml. Advisory only.

- test_csv_with_date_range: regression guard for the elif→if refactor on
  the CSV branch; verifies CSV output still works when explicit date
  params are supplied.
- test_explicit_start_date_ignores_days: when start_date is provided,
  resolve_date_range must skip the days-sentinel path and use start_date
  as-is regardless of days value (including days=0).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

GPT AI Code Review

1. Acceptance criteria

The diff satisfies the acceptance criteria outlined in the linked issue. The following points confirm this:

  • Optional Query Parameters: The start_date and end_date query parameters have been added to the /timeseries/meta endpoint (lines 9-25).
  • Input Validation: The implementation correctly raises an HTTP 422 error when start_date is greater than end_date (lines 25-31).
  • Data Layer Filtering: The resolve_date_range function has been modified to handle the new date parameters and ensure that the filtering logic is pushed to the data layer (lines 162-191).
  • Response Schema: The response schema remains unchanged, as the shape of the returned records is preserved (lines 118-119).
  • Tests: Comprehensive tests have been added to cover all specified scenarios, including happy paths and edge cases (lines 164-235).

2. Bugs and logic errors

No bugs or logic errors were identified in the implementation. The handling of date ranges and the associated logic for deriving start and end dates appears to be correct. The tests also cover edge cases, such as single-day ranges and malformed date strings.

3. API, data, and workflow safety

  • Payload Shapes: The backend response shape remains consistent with the existing API, ensuring that the frontend will not break due to changes in the payload.
  • Local Smoke Tests and Deployment Workflows: The changes do not introduce any new dependencies or alter existing workflows, so they should not break local tests or deployment scripts.
  • Secrets and Permissions: There are no changes that affect secrets, permissions, or CI assumptions.

4. Test coverage

The tests comprehensively cover the acceptance criteria. Each scenario outlined in the criteria is addressed, including:

  • Both dates supplied
  • Open start and end dates
  • No date parameters
  • Invalid date ranges
  • Malformed date strings

The tests are well-structured and utilize mocking effectively to isolate the functionality being tested.

5. Minor issues (optional)

  • The use of | None for type hinting in the query parameters (lines 9-25) is a valid syntax in Python 3.10 and later, but it may not be compatible with earlier versions. If backward compatibility is a concern, consider using Optional[date] instead.
  • The comments in the code are clear and informative, which aids in understanding the logic.

Summary Verdict: APPROVE


Reviewed by GPT via gpt-pr-review.yml. Advisory only.

@github-actions

Copy link
Copy Markdown

Claude AI Code Review

Review: feat: add start_date/end_date query params to /timeseries/meta


1. Acceptance Criteria

AC Status
Optional start_date/end_date query params, ISO-8601
Reject start_date > end_date with 422
Accept None for either bound
Use datetime.date so FastAPI handles parsing
Push filtering to data layer ⚠️ See below
Extract helper (apply_date_range) in service/repository layer ❌ Not done
No response schema changes
Happy path (both dates)
Open start (only end_date)
Open end (only start_date)
Neither supplied (regression guard)
Invalid range → 422
Malformed date → 422

Gap — "push filtering to the data layer": The PR passes start_date/end_date to load_meta_timeseries_range, which is correct plumbing, but the diff doesn't show that function's internals. If it was already doing DB-level filtering, fine — but if it fetches all rows and slices in Python, the AC is unmet and the performance motivation of the issue is not delivered. The PR body should explicitly confirm this.

Gap — apply_date_range helper: The issue explicitly asked for an extracted helper in the service/repository layer. resolve_date_range is extended instead, which is a different function with a different responsibility (computing bounds, not filtering). This is a naming/architecture miss against the stated AC.


2. Bugs and Logic Errors

Critical: validation runs after resolve_date_range can flip the range

# timeseries_meta.py
start_date, end_date = resolve_date_range(
    days, start_date=start_date, end_date=end_date
)

if start_date > end_date:
    raise HTTPException(status_code=422, ...)

resolve_date_range can silently rewrite the caller-supplied dates. If a user passes start_date=2024-01-03 with no end_date, end_date becomes yesterday. If yesterday is 2024-01-02, the resolved pair is (2024-01-03, 2024-01-02) — the 422 fires, but the error message will show the resolved dates, not what the user passed. This is confusing UX and the validation should happen on the raw query params before calling resolve_date_range, or the error message should clearly state "resolved dates" vs "supplied dates".

explicit_end flag is fragile

# timeseries_helpers.py
explicit_end = end_date is not None
if end_date is None:
    end_date = datetime.date.today() - datetime.timedelta(days=1)
if start_date is None:
    if days <= 0:
        start_date = datetime.date(1900, 1, 1)
    elif explicit_end:
        start_date = end_date - datetime.timedelta(days=days)
    else:
        start_date = datetime.date.today() - datetime.timedelta(days=days)

The explicit_end flag is introduced solely to distinguish "caller passed end_date" from "we defaulted it". This is a code smell — the flag exists because the function mutates end_date before using it for the start_date branch. A cleaner fix would be to compute effective_end = end_date or (today - 1 day) without mutating the parameter, then use end_date is not None in-place. The current pattern will confuse future maintainers.

days=0 with explicit start_date — the test is correct but the route-layer check could still fail

test_explicit_start_date_ignores_days correctly verifies days=0 doesn't override an explicit start_date. This works because resolve_date_range only enters the start_date is None branch when start is absent. Good. But this is subtle and not documented in the function's docstring update.

Single-day range boundary condition

test_single_day_range passes start_date == end_date. The guard is start_date > end_date, so equality is allowed — correct. No issue here, just confirming the test exercises the right boundary.


3. API, Data, and Workflow Safety

No frontend changes: The PR title says /timeseries/meta and the diff is backend-only. The endpoint gains optional params with no schema change, so existing frontend callers are unaffected. ✅

elifif refactor on CSV branch (line ~118):

# Before
elif format == "csv":
    ...
# After
if format == "csv":
    ...

The comment explains the JSON path returns early, making elif unreachable — this is correct. But it introduces a subtle dependency on the JSON branch always returning before this if. If someone later adds a non-returning JSON code path (e.g., logging, metrics), the CSV branch could be hit twice or produce unexpected output. An explicit return before the CSV block or a structural comment would make this safer long-term. Low risk now, but worth noting.

_render_meta_html column-padding logic:

for col in ("Open", "High", "Low", "Close"):
    if col not in render_df.columns:
        render_df[col] = float("nan")

This silently pads missing columns. If load_meta_timeseries_range returns a DataFrame that genuinely lacks OHLC columns (e.g., due to a data source change), callers get a NaN-filled HTML table with no error. This is a latent data-quality risk, but it predates this PR's intent (it's a refactor of existing inline HTML generation), so it's not a new regression — just worth flagging.

No CI/deployment changes: No workflow files modified. ✅


4. Test Coverage

The test suite is thorough and directly maps to the ACs:

  • ✅ Both dates supplied
  • ✅ Only end_date
  • ✅ Only start_date
  • ✅ Neither (days-based regression guard)
  • ✅ Invalid range → 422
  • ✅ Single-day range
  • ✅ Malformed date → 422
  • ✅ HTML output contains date range
  • ✅ CSV with date params (regression for elifif refactor)
  • days=0 + explicit start_date doesn't use 1900 sentinel
  • ✅ Spy-based tests verifying load_meta_timeseries_range receives correct dates
  • end_date + days=N derives start correctly
  • days=0 + end_date uses 1900 sentinel

Missing test: There's no test for the confusing 422 case described in Bug #1 above — user supplies start_date=yesterday+1 with no end_date, so resolve_date_range makes end_date=yesterday, triggering 422 with a resolved-date error message. This is a real user-facing footgun that should be explicitly tested and the error message improved.

Missing test: No test verifies the _render_meta_html scaling subtitle appears correctly when scaling != 1.0 with explicit date params (the existing test_timeseries_meta_formats_with_scaling covers the old path, but the new _render_meta_html function's subtitle formatting for combined date+scaling is only indirectly covered).


5. Minor Issues

  • test_explicit_start_and_end_date and test_load_called_with_resolved_dates are nearly identical in what they verify — one via response JSON, one via spy. This is fine for coverage depth but could be noted in a comment.
  • The docstring addition to resolve_date_range is good, but it doesn't mention the new start_date parameter in the Parameters section. The existing params section only documents days and end_date/start_date at a high level — the new explicit_end branching logic for the end_date+days interaction should appear there.
  • Import ordering changes (from fastapi import HTTPException before module imports) match isort conventions. ✅

Verdict

REQUEST CHANGES — The validation-after-resolution bug (user sees a 422 with confusing resolved-date values when supplying a future start_date with no end_date) and the unconfirmed "push filtering to DB layer" AC are blocking; the explicit_end flag smell is a strong recommendation to address before merge.


Reviewed by Claude via claude-pr-review.yml. Advisory only.

leonarduk and others added 2 commits May 27, 2026 21:06
The app has a custom exception handler that maps FastAPI's
RequestValidationError (normally 422) to 400 for unparseable query
params.  Pinning to exactly 422 was incorrect — restore the original
`in (400, 422)` form to accommodate either behaviour.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

GPT AI Code Review

1. Acceptance criteria

The diff satisfies the acceptance criteria outlined in the linked issue. The following points confirm this:

  • Optional Query Parameters: The start_date and end_date parameters have been added to the /timeseries/meta endpoint (lines 9-25).
  • Input Validation: The implementation correctly raises an HTTP 422 error when start_date is greater than end_date (lines 25-31).
  • Data Layer Filtering: The resolve_date_range function has been updated to handle the new parameters and filter data accordingly (lines 162-191).
  • Response Schema: The response schema remains unchanged, as the shape of the returned records is preserved (lines 153-153).
  • Tests: Comprehensive tests have been added to cover all specified scenarios, including happy paths and edge cases (lines 164-239).

2. Bugs and logic errors

The implementation appears to handle the expected logic correctly. However, there are a couple of edge cases worth noting:

  • Single Day Range: The test for a single-day range (lines 164-164) is valid, but it would be prudent to ensure that the backend correctly handles this case without any issues.
  • Malformed Date Handling: The test for malformed date strings (lines 164-164) is appropriately handled, but it would be beneficial to ensure that the error message returned is clear and informative.

3. API, data, and workflow safety

  • Payload Shape Alignment: The changes do not alter the existing payload shape, ensuring that the backend and frontend contracts remain aligned.
  • Local Smoke Tests: The changes should not break local smoke tests or deployment workflows, as the existing functionality is preserved, and new parameters are optional.
  • Secrets and Permissions: There are no changes to secrets or permissions, and the implementation adheres to existing CI assumptions.

4. Test coverage

The test coverage is comprehensive and addresses all acceptance criteria. The following scenarios are well-covered:

  • Both start_date and end_date supplied.
  • Only end_date supplied (open start).
  • Only start_date supplied (open end).
  • Neither date supplied (regression guard).
  • Invalid range (start_date > end_date).
  • Malformed date string.

All tests are appropriately structured to validate the new functionality.

5. Minor issues (optional)

  • Code Style: The code style is consistent with the existing codebase, and no significant issues were noted.
  • Documentation: The inline comments are clear and helpful, but it might be beneficial to add a brief comment explaining the purpose of the new parameters in the route definition for future maintainers.

Summary Verdict: APPROVE


Reviewed by GPT via gpt-pr-review.yml. Advisory only.

@github-actions

Copy link
Copy Markdown

Claude AI Code Review

Review: feat: add start_date/end_date query params to /timeseries/meta


1. Acceptance Criteria

AC Status
Optional start_date/end_date query params, existing callers unbroken
422 on start_date > end_date
Accept None for either bound
datetime.date type with FastAPI auto-parsing
Push filtering to data layer ⚠️ See below
apply_date_range helper in service/repo layer ❌ Not implemented
Response schema unchanged
Happy path / open-start / open-end / neither / invalid-range / malformed tests ✅ All present

Gap — AC #3/4 ("push filtering to the data layer", helper extraction):
The route resolves the date bounds but there is no evidence in the diff that load_meta_timeseries_range now uses those bounds to filter at the query/ORM level. The spy tests in the new test suite (test_load_called_with_resolved_dates, etc.) only verify that the resolved dates are passed to the loader — they don't verify the loader actually uses them. If the underlying loader was already range-aware, this is fine, but the PR makes no change to it and the issue explicitly requires this. This is a gap worth calling out.

AC #4 ("extract helper apply_date_range"): Absent. The issue says to extract this helper in the service/repository layer. The logic is embedded in resolve_date_range in timeseries_helpers.py, which is a different concern (it's a date resolution helper, not a query filtering helper). Not a hard blocker if the loader already filters, but the issue called it out explicitly.


2. Bugs and Logic Errors

Variable shadowing — significant bug:
In get_meta_timeseries, the query parameters are named start_date and end_date, and then resolve_date_range is called with those as keyword arguments, reassigning the same names:

# Line ~64-66 (route signature)
start_date: date | None = Query(None, ...),
end_date: date | None = Query(None, ...),
...
start_date, end_date = resolve_date_range(days, start_date=start_date, end_date=end_date)

This works, but then the validation check:

if start_date > end_date:
    raise HTTPException(status_code=422, ...)

...is placed after resolve_date_range. This means the 422 guard is comparing the resolved dates, not the raw inputs. That's actually correct behavior (since resolve_date_range may fill in a default end_date of yesterday), but it creates a subtle issue: if the user passes start_date=2024-01-10 with no end_date, the resolved end_date becomes yesterday. If yesterday is before 2024-01-10, the 422 fires — which is correct — but the error message will show the resolved end_date (e.g., "end_date (2023-12-01)") rather than making it clear that no end_date was supplied. This could confuse callers. The test test_open_end_only_start_date uses start_date=2024-01-01 and will pass when run today, but will silently break when "today - 1 day" is before 2024-01-01 in some future test environment or time-frozen CI context.

_render_meta_html has a dead code block:
The refactored function still contains the old inline HTML generation:

# Lines ~140-152 in the diff
html_table = df.to_html(index=False)
safe_symbol = html.escape(f"{ticker}.{exchange}")
safe_date_range = html.escape(f"{start_date} to {end_date}")
...
"""

This HTML string is built but never used — render_timeseries_html is called instead. The docstring opening ("""Render the meta timeseries...) is also never closed properly before the dead code begins, which would be a SyntaxError. Looking more carefully at the diff context:

def _render_meta_html(...) -> HTMLResponse:
    """Render the meta timeseries as HTML using the shared render helper.

    Pads any missing standard columns with safe defaults so
    render_timeseries_html never raises KeyError on this DataFrame.
    html_table = df.to_html(index=False)   # <-- this is INSIDE the docstring

The docstring is missing its closing """ before html_table = df.to_html(...). This means all that code through the HTML template string is part of the docstring, not executable code. This is either a diff truncation artifact or a real syntax issue. This needs clarification. If it's a real syntax error, it would cause an import failure.

elifif refactor on CSV branch:
The change from elif format == "csv" to if format == "csv" is noted with a comment, and test_csv_with_date_range exists. However, if format == "json" and the JSON path does not return early for some reason (e.g., an exception path is changed), both JSON and CSV branches could execute. The test covers the happy path, but the logic is slightly more fragile than the original elif chain. This is low risk but worth noting.


3. API, Data, and Workflow Safety

Backend/frontend contract: The PR title says /timeseries/meta only. No frontend changes are in the diff. The new params are additive and optional, so existing callers are unaffected. ✅

days param interaction — underdocumented breaking change in semantics:
The new resolve_date_range behavior when end_date is supplied changes what days means: it now anchors relative to end_date rather than today. This is a semantic change to the function signature and is documented in the docstring, but the existing callers of resolve_date_range (if any others exist) may not expect this. The diff doesn't show a grep for other callers. If this function is called elsewhere with days and without end_date, behavior is unchanged — but if any caller passes end_date, the meaning of days has silently shifted.

No filtering in the data layer: As noted in AC section, the dates are passed to load_meta_timeseries_range but there's no evidence in this diff that the function does anything with them at the DB/query level. If it previously ignored those params and filtered post-fetch, nothing has changed. The PR should confirm this.


4. Test Coverage

Tests are thorough and directly map to the AC list. Specific praise:

  • test_explicit_start_date_ignores_days — correctly guards against the days=0 sentinel overriding explicit start_date
  • test_days_zero_with_end_date_means_all_history — pins the sentinel behavior
  • test_csv_with_date_range — explicitly calls out the elifif regression risk

Missing test — future date start_date:
test_open_end_only_start_date passes start_date=2024-01-01 which is in the past, so the resolved end_date (yesterday) will be after it. There's no test for start_date set to a future date, which would trigger the 422 after resolve_date_range sets end_date = yesterday. This is a real user scenario (e.g., "give me next week's data") and worth a test.

Missing test — data layer actually filters:
There is no test asserting that the returned JSON records are bounded by the date range. The spy tests verify the dates are passed but not applied. A test asserting len(data["data"]) == expected or checking record dates against the bounds would close this gap.


5. Minor Issues

  • The _multi_day_df() helper is defined twice — once inline at line ~167 and reused, but _client_with_spy tests also call it. If it's module-level that's fine; just confirm it's not copy-pasted.
  • Import ordering in tests (e.g., from datetime import date inside test functions rather than at top of file) is inconsistent but not harmful.
  • The test_timeseries_meta_formats_with_scaling assertion change ("Scaling:</strong> 2.0x""scaling: 2.0x") silently changes the expected HTML structure. This is fine if render_timeseries_html produces that output, but it's a behavior change that should be explicitly noted in the PR description as an intentional side-effect of the _render_meta_html refactor.

Summary

REQUEST CHANGES. The docstring-swallowing-code issue in _render_meta_html is a potential syntax/runtime bug that needs verification, the data-layer filtering requirement from the issue AC is not demonstrably met in this diff, and the start_date in the future producing an opaque 422 against a resolved end_date is a real usability issue without a test or guard.


Reviewed by Claude via claude-pr-review.yml. Advisory only.

@github-actions

Copy link
Copy Markdown

GPT AI Code Review

1. Acceptance criteria

The diff satisfies the acceptance criteria outlined in the linked issue. The following points confirm this:

  • Optional Query Parameters: The start_date and end_date parameters were added to the /timeseries/meta endpoint (lines 9-25).
  • Input Validation: The implementation correctly raises an HTTP 422 error when start_date is greater than end_date (lines 25-30). It also handles cases where either date is None.
  • Data Layer Filtering: The logic for filtering based on the date range is pushed to the data layer via the resolve_date_range function (lines 57-119).
  • Response Schema: The response schema remains unchanged, as confirmed by the tests (lines 164-239).
  • Tests: Comprehensive tests were added to cover all specified scenarios, including happy paths and edge cases (lines 164-239).

2. Bugs and logic errors

No bugs or logic errors were identified in the implementation. The handling of date ranges, including the logic for deriving start_date when only end_date is provided, appears sound. The handling of edge cases, such as start_date equal to end_date, is also correctly implemented.

3. API, data, and workflow safety

  • The backend response shape remains consistent with existing contracts, ensuring that frontend components relying on this data will not break.
  • The changes do not appear to affect local smoke tests, deployment workflows, or repo scripts adversely. The implementation adheres to the existing conventions and practices in the codebase.
  • There are no concerns regarding secrets, permissions, or CI assumptions in the changes made.

4. Test coverage

The test coverage is thorough and addresses all acceptance criteria:

  • Tests for both start_date and end_date supplied.
  • Tests for open-ended ranges (only one date supplied).
  • Tests for no date parameters, ensuring existing behavior is preserved.
  • Tests for invalid date ranges and malformed date strings, ensuring proper error handling.

All scenarios outlined in the acceptance criteria are covered by the tests.

5. Minor issues (optional)

No significant minor issues were noted. The code is well-structured, and naming conventions are consistent with the existing codebase. The comments added to the code enhance clarity.

Summary Verdict: APPROVE


Reviewed by GPT via gpt-pr-review.yml. Advisory only.

@github-actions

Copy link
Copy Markdown

Claude AI Code Review

Review: feat: add start_date/end_date query params to /timeseries/meta

1. Acceptance Criteria

Working through the issue checklist:

  • ✅ Optional query params added (Optional[date] via date | None = Query(None))
  • ✅ 422 on start_date > end_date
  • ✅ Open-ended ranges (either bound None) accepted
  • date type used so FastAPI handles parsing errors
  • ✅ Response schema unchanged
  • ✅ Happy path, open-start, open-end, neither-supplied, invalid-range, and malformed-date tests all present

Gap — AC item 3 ("push filtering to the data layer"): The PR adds date params to the route and passes them to load_meta_timeseries_range, but the diff shows no changes to load_meta_timeseries_range itself or the repository/query layer. The route already called load_meta_timeseries_range(ticker, exchange, start_date, end_date, ...) before this PR, so the filtering may have already been there — but the issue explicitly requires verifying this, and nothing in the diff confirms it. If the underlying loader was fetching all rows and filtering in Python, that's still happening. This should be confirmed or called out.

Gap — AC item 4 ("extract apply_date_range helper in service/repository layer"): The issue specifically asks for this helper. The date logic stays in resolve_date_range in timeseries_helpers.py, which is a utility function, not a service/repository helper that sits at the data access boundary. Minor concern, but the AC is literally unmet as written.


2. Bugs and Logic Errors

Shadowing of start_date/end_date query parameters — real bug:

# Route signature:
start_date: date | None = Query(None, ...)
end_date: date | None = Query(None, ...)

# Then immediately:
start_date, end_date = resolve_date_range(days, start_date=start_date, end_date=end_date)

# Then:
if start_date > end_date:
    raise HTTPException(...)

The variable names start_date and end_date are reused for both the raw query params and the resolved dates. This is fine at runtime, but the 422 check fires on the resolved dates, not the supplied ones. Consider the case where the caller supplies start_date=2024-01-10 with no end_date: resolve_date_range sets end_date to yesterday. If yesterday < 2024-01-10, the 422 fires — but the caller didn't supply an invalid range; they supplied a future start date with an implicit end date. The error message will say something like start_date (2024-01-10) must not be after end_date (2024-01-09), which is confusing because the user never set end_date. This is arguably the correct behaviour (the resolved window is empty/invalid) but the error message should reflect that end_date was inferred, not supplied.

_render_meta_html docstring is incomplete:

def _render_meta_html(...) -> HTMLResponse:
    """Render the meta timeseries as HTML using the shared render helper.

    Pads any missing standard columns with safe defaults so
    render_timeseries_html never raises KeyError on this DataFrame.
    html_table = df.to_html(index=False)  # ← this is code, not a docstring

The closing """ of the docstring is missing from the diff. The docstring bleeds into the function body, meaning html_table = df.to_html(index=False) through the HTML template literal are treated as part of the docstring string, not executed code. The function body would then start at render_df = df.copy(). This means html_table, safe_symbol, safe_date_range, and the html_doc template are dead code — they're in the docstring. This is a real bug — those variables are defined but never used in the actual function body, and the old HTML rendering path using those variables is silently dropped. The function will still return a response (via render_timeseries_html) but the intermediate variables serve no purpose and the docstring is malformed.

Checking more carefully: the diff shows the docstring opens at line ~128 and the html_table = ... line appears inside the docstring block (the diff shows it without a """ closing before it). The function would only execute render_df = df.copy() onward.

elifif refactor for CSV branch:

# Old:
elif format == "csv":
    ...
# New:
if format == "csv":
    ...

The JSON path does return before this, so the CSV if is only reached when format != "json". This is functionally correct. The test test_csv_with_date_range covers it. Fine.

test_invalid_range_returns_422 lacks message assertion:

The test only checks status_code == 422. Given the documented concern about misleading error messages (resolved vs supplied dates), it would be worth asserting that detail is present and non-empty. Minor but relevant for the "clear message" AC.


3. API, Data, and Workflow Safety

No frontend changes: The PR title says /timeseries/meta and the diff touches only backend files. If any frontend component calls /timeseries/meta, the new params are additive and backward-compatible — existing callers work unchanged. ✅

resolve_date_range signature change: The function now accepts start_date and end_date kwargs. Any existing callers of resolve_date_range that pass positional args beyond days could break. The diff doesn't show the full original signature, so it's worth verifying no other callers pass positional start_date/end_date.

days <= 0 with explicit end_date: The PR correctly maps to the date(1900, 1, 1) sentinel regardless. Covered by test_days_zero_with_end_date_means_all_history. ✅

No secrets/CI concerns.


4. Test Coverage

Tests are thorough and cover all AC scenarios. A few observations:

  • test_explicit_start_date_ignores_days and test_load_called_with_resolved_dates both create their own TestClient via create_app() rather than using _client_with_spy. This is redundant — _client_with_spy was introduced in the same PR for exactly this purpose. The test_explicit_start_date_ignores_days test duplicates the monkeypatching setup manually.

  • The malformed date test correctly accepts 400 or 422 with a comment explaining why. ✅

  • No test exercises the "future start_date with implicit end_date" edge case described in §2 (the confusing 422 path). Worth adding.

  • _multi_day_df() lines exceed PEP 8's 79-char limit ({"Date": "2024-01-01", "Open": 1.0, "High": 2.0, "Low": 0.5, "Close": 1.5, "Volume": 100}). Minor, but make lint may flag it depending on configuration.


5. Minor Issues

  • The _render_meta_html function accepts start_date: date and end_date: date but only uses them to build subtitle. The start_date/end_date parameters could be replaced with a pre-built subtitle: str to keep the function's responsibility cleaner.

  • Import ordering in test_timeseries_meta.py changed (from fastapi import HTTPException moved before the module import). This is consistent with isort's convention (stdlib/third-party before local). ✅


Summary

REQUEST CHANGES — the docstring-swallowing-function-body bug in _render_meta_html (dead html_table/safe_symbol/html_doc variables, malformed docstring) is a real defect that needs fixing before merge, and the "filtering at the data layer" AC should be explicitly confirmed or addressed.


Reviewed by Claude via claude-pr-review.yml. Advisory only.

An old HTML-template block (html_doc = f\"\"\"...\"\"\") was accidentally
embedded inside the function's docstring.  The inner f\"\"\" terminated
the docstring early, leaving the rest of the file inside an unclosed
triple-quoted literal and producing:

  SyntaxError: unterminated triple-quoted string literal (detected at line 209)

Remove the stray code from the docstring and drop the now-unused
`import html` that was re-introduced alongside it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

GPT AI Code Review

1. Acceptance criteria

The diff satisfies the acceptance criteria outlined in the linked issue. The following points confirm this:

  • Optional Query Parameters: The start_date and end_date parameters have been added to the /timeseries/meta endpoint (lines 57-63).
  • Input Validation: The code correctly raises an HTTP 422 error when start_date is greater than end_date (lines 66-73).
  • Data Layer Filtering: The resolve_date_range function has been modified to incorporate the new date parameters and ensure that the database handles filtering (lines 162-191).
  • No Response Schema Changes: The response schema remains unchanged, as the shape of the returned records is preserved (lines 95-100).
  • Tests: Comprehensive tests have been added to cover all specified scenarios, including happy paths and edge cases (lines 164-239).

2. Bugs and logic errors

The implementation appears to handle edge cases correctly, such as:

  • Open-ended date ranges (only start_date or end_date supplied).
  • The case where start_date equals end_date is treated as valid.
  • The handling of malformed date strings returns appropriate HTTP errors.

However, the comment in resolve_date_range regarding the handling of days could be clearer. It states that days <= 0 always maps to "all available history," but it might be beneficial to explicitly mention how this interacts with the new date parameters.

3. API, data, and workflow safety

  • The backend and frontend payload shapes remain aligned, as the response structure does not change.
  • The addition of query parameters and validation should not break existing workflows, as they are optional and existing callers will continue to function as before.
  • There are no apparent issues with secrets, permissions, or CI assumptions in the changes made.

4. Test coverage

The tests added cover all acceptance criteria, including:

  • Valid date ranges with both parameters.
  • Open-ended ranges with one parameter.
  • The case where neither parameter is supplied.
  • Invalid date ranges and malformed date strings.

All scenarios specified in the acceptance criteria are addressed, ensuring robust coverage.

5. Minor issues (optional)

  • The comment in resolve_date_range could be improved for clarity regarding the interaction of days with the new parameters (lines 162-191).
  • The use of | for type hinting (e.g., date | None) is valid in Python 3.10+, but it might be worth ensuring that the codebase is consistently using this syntax if the project supports earlier versions.

Summary Verdict: APPROVE


Reviewed by GPT via gpt-pr-review.yml. Advisory only.

@github-actions

Copy link
Copy Markdown

Claude AI Code Review

Review: feat: add start_date/end_date query params to /timeseries/meta

1. Acceptance Criteria

Going through the linked issue ACs one by one:

✅ Optional query parameters with Optional[date] = Query(None) — implemented, existing callers unaffected.

✅ Validate start_date > end_date → 422 — present in route layer.

✅ Accept None for either bound — handled via resolve_date_range.

✅ FastAPI handles parse errors automatically — confirmed by test_malformed_date_returns_4xx.

⚠️ "Push filtering to the data layer" — This AC is not met. The PR passes start_date/end_date to load_meta_timeseries_range, but there's no evidence in the diff that load_meta_timeseries_range actually filters at the DB/query level. The spy tests only assert what arguments are passed; they don't verify filtering happens in the data layer. If load_meta_timeseries_range already accepted these params and filtered at the DB layer before this PR, that should be called out explicitly. If it doesn't, this is a gap against the stated AC.

✅ Keep domain logic out of routesresolve_date_range helper used appropriately.

✅ Response schema unchanged — only record count changes.

✅ All six test categories present — happy path, open start, open end, neither, invalid range, malformed date.

Missing: The issue says "Closes #2747" should appear in the PR body. The diff doesn't show a PR description, but the checklist item should be flagged.


2. Bugs and Logic Errors

Significant: Variable shadowing of query parameters

# Route signature (line ~56-61):
start_date: date | None = Query(None, ...),
end_date: date | None = Query(None, ...),
...
# Then immediately:
start_date, end_date = resolve_date_range(days, start_date=start_date, end_date=end_date)

This is correct in isolation, but the 422 validation check on line ~67 now compares the resolved dates, not the user-supplied ones. That's actually the right behavior (you want to catch cases where resolution produces an inversion), but the docstring on end_date says "Defaults to yesterday when omitted" — if a user passes start_date=today (no end_date), the resolved end_date will be yesterday, which is before start_date, and they'll get a 422 with a confusing message showing dates they didn't provide. This is a real usability hazard.

Example:

GET /timeseries/meta?ticker=ABC&exchange=L&start_date=2025-07-14

Today is 2025-07-14, yesterday is 2025-07-13. The user gets:

422: start_date (2025-07-14) must not be after end_date (2025-07-13)

...with no explanation that end_date was defaulted to yesterday. The error message should clarify when defaults were applied, or the validation should happen before resolution and handle the today/yesterday boundary differently.

resolve_date_range semantics conflict with days=0 + explicit end_date

In timeseries_helpers.py, the new logic:

explicit_end = end_date is not None
if end_date is None:
    end_date = datetime.date.today() - datetime.timedelta(days=1)
if start_date is None:
    if days <= 0:
        start_date = datetime.date(1900, 1, 1)
    elif explicit_end:
        start_date = end_date - datetime.timedelta(days=days)
    else:
        start_date = datetime.date.today() - datetime.timedelta(days=days)

When days=0 and explicit_end=True, it still hits the days <= 0 branch and returns date(1900, 1, 1). The test test_days_zero_with_end_date_means_all_history asserts this is intentional. But days=0 semantically means "all history" regardless — fine — however the docstring says days <= 0 means "all available history" without qualification. This interaction is tested but the priority order (days <= 0 trumps explicit_end) deserves an explicit comment in the code, not just the docstring block above.

elifif refactor on the CSV branch

# Before: elif format == "csv":
# After:  if format == "csv":

The comment says "JSON path returns above" which is correct — JSON returns early. But this is a subtle control-flow dependency: if someone later adds a non-returning branch before this if, the CSV and HTML paths could both execute. The elif was safer. The test test_csv_with_date_range covers this, but the structural fragility is worth noting.


3. API, Data, and Workflow Safety

Backend/frontend contract: This endpoint appears to be consumed internally (HTML/CSV/JSON for display). The JSON response shape is unchanged (from, to, records). No frontend changes in the diff, which is appropriate.

import html removal: The diff removes import html but the old HTML rendering used html.escape(). The new _render_meta_html delegates to render_timeseries_html. If render_timeseries_html doesn't escape user-controlled values like ticker and exchange, there's a potential XSS regression. This needs to be verified — the diff doesn't show render_timeseries_html's implementation.

subtitle = f"{start_date} to {end_date}"  # date objects, safe
# But:
f"{ticker}.{exchange} Price History"  # ticker/exchange from _resolve_ticker_exchange — are these escaped inside render_timeseries_html?

No frontend changes needed: Confirmed — the new params are purely additive query params.

CI/deployment: No workflow file changes. No new dependencies. Safe.


4. Test Coverage

The six required test categories are all present and well-structured. The spy-based tests (test_load_called_with_resolved_dates, test_end_date_with_days_derives_start) are good engineering.

Missing test: The "today boundary" bug described above — start_date=today with no end_date yielding a confusing 422 — has no test. Given this is a real user-facing footgun, it should either be tested as "expected 422 with helpful message" or fixed.

Missing test: There's no test verifying that resolve_date_range is tested directly in tests/utils/ for the new branches. The route tests exercise it indirectly, but unit tests for resolve_date_range itself would make future refactors safer, especially given the new branching complexity.

test_timeseries_meta_formats_with_scaling assertion change (line ~107):

# Before: assert "Scaling:</strong> 2.0x" in resp.text
# After:  assert "scaling: 2.0x" in resp.text

This is testing a different HTML structure. The test change is valid given the refactor, but it means the old HTML structure is no longer verified anywhere. If render_timeseries_html changes its output format, this test won't catch a regression in scaling display.


5. Minor Issues

  • The date | None union syntax (PEP 604) requires Python 3.10+. If the repo targets 3.9, this should be Optional[date] for consistency. The existing route params use Query(None) but the type annotation style matters for older Python versions.

  • _render_meta_html is a module-level private function that pads DataFrame columns. The padding logic (adding NaN OHLC columns) is defensive but slightly surprising — if load_meta_timeseries_range always returns these columns, the padding is dead code; if it sometimes doesn't, that's a bug in the loader that should be fixed there, not papered over here.

  • The test file whitespace normalization (blank line moves around imports) is noise but harmless.


Summary Verdict

REQUEST CHANGES — Two blocking concerns: (1) the "today as start_date" boundary produces a cryptic 422 that will confuse real users and has no test, and (2) the XSS risk from removing html.escape needs to be confirmed safe by reviewing render_timeseries_html's escaping behavior. The data-layer filtering AC also needs explicit confirmation or a code pointer.


Reviewed by Claude via claude-pr-review.yml. Advisory only.

@github-actions

Copy link
Copy Markdown

GPT AI Code Review

1. Acceptance criteria

The diff satisfies all acceptance criteria outlined in the linked issue. The implementation correctly adds optional start_date and end_date query parameters to the /timeseries/meta endpoint, validates the input, and filters the data at the database level. The response schema remains unchanged, and the tests cover all specified scenarios, including:

  • Both dates supplied
  • Open start (only end_date supplied)
  • Open end (only start_date supplied)
  • Neither supplied (regression guard)
  • Invalid range (start_date > end_date)
  • Malformed date string

2. Bugs and logic errors

The implementation appears to handle edge cases correctly. The logic for deriving start_date when only end_date is provided is well-defined, ensuring that the behavior remains consistent with the existing functionality. The handling of the days parameter when dates are explicitly supplied is also correctly implemented.

One minor point to note is that the comment in resolve_date_range regarding the behavior when days <= 0 could be clearer. It states that it always maps to "all available history," but it might be beneficial to explicitly mention that this behavior is preserved even when end_date is provided.

3. API, data, and workflow safety

The changes maintain alignment between backend and frontend payload shapes, as the response schema remains unchanged. The filtering is done at the database level, which is efficient and avoids unnecessary data transfer. There are no indications that this PR would break local smoke tests, deployment workflows, or repo scripts. The handling of secrets and permissions appears to be unaffected by these changes.

4. Test coverage

The tests added in test_timeseries_meta.py comprehensively cover the acceptance criteria. Each scenario is well-represented, including edge cases like invalid date ranges and malformed date strings. The tests also ensure that the expected behavior is maintained when neither date parameter is supplied.

5. Minor issues (optional)

The code is generally well-structured and follows existing conventions. However, the comment in resolve_date_range could be improved for clarity, as mentioned earlier. Additionally, the use of date | None for type hinting is valid in Python 3.10+, but if the project aims for compatibility with earlier versions, it might be worth using Optional[date] instead.

Summary verdict: APPROVE


Reviewed by GPT via gpt-pr-review.yml. Advisory only.

@github-actions

Copy link
Copy Markdown

Claude AI Code Review

Review: feat: add start_date/end_date query params to /timeseries/meta


1. Acceptance Criteria

Going through the issue ACs:

AC Status
Optional start_date/end_date query params (ISO-8601)
Don't break existing callers (params are optional)
Reject start_date > end_date with 422
Accept None for either bound
Use datetime.date so FastAPI handles parsing
Push filtering to data layer Gap — see below
Extract helper in service/repo layer ⚠️ Partial
Don't change response schema
Happy path (both dates)
Open start (end_date only)
Open end (start_date only)
Neither supplied (regression guard)
Invalid range → 422
Malformed date → 422

Gap — "push filtering to the data layer": The issue explicitly requires that date bounds be passed into the query/ORM call so the database does the filtering. Looking at the diff, resolve_date_range now computes the correct (start_date, end_date) pair and those values are passed to load_meta_timeseries_range. However, the diff shows no changes to load_meta_timeseries_range itself. If that function already accepts and uses start_date/end_date to filter at the DB/query level, this is fine — but the PR should confirm this (and the diff doesn't). If it fetches all rows and the caller post-filters, this AC is unmet.

apply_date_range helper AC: The issue asks for a helper like apply_date_range(query, start_date, end_date) in the service/repository layer. resolve_date_range in timeseries_helpers.py was extended instead. That's a reasonable call if load_meta_timeseries_range already handles filtering, but it's not the same contract as described.


2. Bugs and Logic Errors

Validation bypass when start_date is explicit but end_date resolves to yesterday

In get_meta_timeseries, the 422 check fires after resolve_date_range. If the caller passes start_date=2024-01-03 with no end_date, and today is 2024-01-02, then end_date resolves to yesterday (2024-01-01), giving start_date > end_date. The 422 fires correctly — but the error message will say start_date (2024-01-03) must not be after end_date (2024-01-01) with no mention that end_date was inferred, which will confuse callers. Consider making the message explicit: "end_date defaults to yesterday (2024-01-01)".

resolve_date_range variable shadowing in the route

# route signature
start_date: date | None = Query(None, ...)
end_date: date | None = Query(None, ...)

# then immediately:
start_date, end_date = resolve_date_range(days, start_date=start_date, end_date=end_date)

This rebinds the parameter names. It works, but any future reader adding code between the Query declaration and the resolve_date_range call could easily read the pre-resolved None values by mistake. A small rename (raw_start, raw_end → resolved into start_date, end_date) would make this safer. Not blocking, but worth noting.

elifif refactor on CSV branch (lines ~120-123)

The comment says "JSON path returns above" justifying the change from elif format == "csv" to if format == "csv". That's true for the JSON branch, but the HTML path used to be a plain else. Now the HTML render is the fallthrough after the if format == "csv" block. This is functionally correct only because _render_meta_html always returns and the CSV block also returns. But there's no guard: if format is somehow neither "csv" nor "html" (currently impossible given the regex constraint, but possible if the regex is relaxed later), the function would fall through to _render_meta_html silently. The original elif/else made the branching explicit and exhaustive. This is a minor robustness regression.

test_explicit_start_date_ignores_days patches ts_meta.pd.to_datetime

monkeypatch.setattr(ts_meta.pd, "to_datetime", lambda x: x)

This is a very broad patch — it disables pd.to_datetime globally within that module for the test's duration. If _render_meta_html or any other code path in timeseries_meta.py calls pd.to_datetime expecting actual datetime conversion, this will silently return the raw string. Given the test only checks captured["start_date"] and doesn't inspect the response body deeply, a broken to_datetime call could hide a real error. This pattern is duplicated across several tests. It's probably fine in practice but is fragile.


3. API, Data, and Workflow Safety

Response schema: no change — the JSON keys (from, to, rows, etc.) are unchanged. ✅

Backward compatibility: Both new params default to None, so existing callers passing only days continue to work. resolve_date_range correctly preserves the original logic for the no-explicit-date path. ✅

import html removal: The diff removes import html at the top of timeseries_meta.py. The old inline HTML used html.escape(); the new _render_meta_html delegates to render_timeseries_html. Verify that render_timeseries_html escapes user-controlled content (ticker, exchange, dates) — if it doesn't, this is an XSS regression. The diff doesn't show render_timeseries_html's implementation, so this needs to be confirmed.

No frontend changes: The PR title says /timeseries/meta only, and the linked issue is backend-focused. The frontend isn't using these params yet, so no contract breakage. ✅

CI/deployment: No workflow file changes. Existing routes are unchanged in shape. ✅


4. Test Coverage

The test suite is thorough and covers all six ACs from the issue. A few observations:

test_open_end_only_start_date doesn't assert the actual end_date value. It only checks data["from"] == "2024-01-01". Since end_date resolves to yesterday (time-dependent), a spy-based assertion like the other tests would be more robust.

No test for start_date == end_date with content returned: test_single_day_range checks the status and date echoes but doesn't verify the data rows — the mock always returns 3 rows regardless. This is fine for a contract test but worth noting.

No test for the HTML XSS surface: A test passing ticker or exchange with HTML-special characters (<, >, &) and asserting they're escaped in the HTML response would guard the import html removal.

_client_with_spy is defined but almost identical to _client_with_df: The duplication is minor, but these could be consolidated.

test_days_zero_with_end_date_means_all_history: Good explicit test for the sentinel value. ✅


5. Minor Issues

  • The days parameter description says "Overrides days when provided" on start_date. It should say "Overrides the days-derived start when provided" or similar for clarity.
  • The end_date description says "Defaults to yesterday when omitted" — accurate, but slightly misleading when start_date is also omitted with days supplied (the behaviour is correct, the doc is just imprecise about the interaction).
  • Import ordering in the test file was adjusted (stdlib before third-party) — good cleanup.

Summary Verdict

REQUEST CHANGES

The primary blocker is that the diff doesn't demonstrate (or test) that load_meta_timeseries_range actually filters at the data layer — the issue's explicit requirement. Additionally, the removal of html.escape in favour of render_timeseries_html introduces a potential XSS surface that needs verification. The logic is otherwise sound and the test coverage is genuinely good.


Reviewed by Claude via claude-pr-review.yml. Advisory only.

@github-actions

Copy link
Copy Markdown

GPT AI Code Review

1. Acceptance criteria

The diff satisfies the acceptance criteria outlined in the linked issue. The following points confirm this:

  • Optional Query Parameters: The start_date and end_date parameters are added to the /timeseries/meta endpoint (lines 58-66).
  • Input Validation: The check for start_date > end_date raises an HTTP 422 error with a clear message (lines 64-70).
  • Data Layer Filtering: The resolve_date_range function is updated to handle the new parameters and correctly derive the date range (lines 162-191).
  • Response Schema: The response schema remains unchanged, as confirmed by the tests (lines 164-239).
  • Tests: Tests are added to cover various scenarios, including valid date ranges, open-ended queries, and error cases (lines 164-239).

2. Bugs and logic errors

There are no apparent bugs or logic errors in the implementation. The handling of date ranges appears correct, and the logic for deriving start and end dates is well-structured. The tests also cover edge cases, such as single-day ranges and malformed date strings.

3. API, data, and workflow safety

  • Payload Shapes: The backend response shape remains consistent with previous implementations, ensuring that frontend expectations are met.
  • Local Smoke Tests: The changes should not break local smoke tests, as the existing functionality is preserved when no date parameters are supplied.
  • Secrets and Permissions: There are no changes to secrets or permissions in this PR, and CI assumptions remain intact.

4. Test coverage

The tests comprehensively cover the acceptance criteria:

  • Happy path scenarios are tested with both dates supplied.
  • Open-ended scenarios are tested with only one date supplied.
  • A regression guard is in place for the case where neither date is supplied.
  • Invalid ranges and malformed dates are also tested, ensuring robust validation.

5. Minor issues (optional)

There are no significant minor issues to note. The code is well-structured and adheres to the existing conventions.

Summary Verdict: APPROVE


Reviewed by GPT via gpt-pr-review.yml. Advisory only.

@github-actions

Copy link
Copy Markdown

Claude AI Code Review

Code Review: feat: add start_date/end_date query params to /timeseries/meta


1. Acceptance Criteria

AC Status
Optional start_date/end_date query params (ISO-8601, not required)
Reject start_date > end_date with HTTP 422
Accept None for either bound
Use datetime.date so FastAPI handles parse errors
Filter pushed to data layer (pass bounds to loader)
apply_date_range helper extracted to service/repository layer Gap
Response schema unchanged
Tests: happy path both dates
Tests: open start (end only)
Tests: open end (start only)
Tests: neither supplied (regression guard)
Tests: invalid range → 422
Tests: malformed date → 4xx

Notable gap: The issue explicitly requires extracting a helper like apply_date_range(query, start_date, end_date) in the service or repository layer. The PR instead calls resolve_date_range (which already existed) and passes the bounds directly into the loader from the route. There's no new helper that encapsulates filtering logic at the data layer. If the intent was that "date range filtering logic lives in the route, loader handles the rest", that diverges from the stated design requirement and could lead to duplication if other endpoints need the same feature later (the issue title says /timeseries/meta but the issue body says "relevant API endpoints" plural).


2. Bugs and Logic Errors

Shadowing of query parameters by resolve_date_range — This is the most significant logic issue.

In timeseries_meta.py, the route signature declares start_date and end_date as parameters, then immediately overwrites them:

# route params:
start_date: date | None = Query(None, ...)
end_date: date | None = Query(None, ...)

# then:
start_date, end_date = resolve_date_range(days, start_date=start_date, end_date=end_date)

# then:
if start_date > end_date:
    raise HTTPException(422, ...)

The 422 guard fires after resolve_date_range has already filled in defaults. This creates a subtle case: if a caller sends start_date=2024-01-05 with no end_date, resolve_date_range sets end_date = today - 1. If today is 2024-01-04, then start_date (2024-01-05) > end_date (2024-01-03) and the request gets a 422 — even though the caller supplied only one bound, which the AC explicitly says must be accepted as an "open-ended range." The 422 check should operate on the raw user-supplied values before defaults are applied, not on the resolved values.

Fix: Check the raw inputs before calling resolve_date_range:

if start_date is not None and end_date is not None and start_date > end_date:
    raise HTTPException(status_code=422, detail=...)
start_date, end_date = resolve_date_range(days, start_date=start_date, end_date=end_date)

elifif refactor on CSV branch (lines ~120–122): The comment claims "JSON path returns above" justifies changing elif to if. This is correct for the JSON path (which does return inside the if format == "json" block), but the HTML path below is now a bare return statement from _render_meta_html. The control flow is sound, but it means an unrecognised format value (which can't happen given the regex constraint ^(html|json|csv)$) would fall through to _render_meta_html implicitly. Not a real bug given the regex guard, but the refactor is slightly fragile — worth a comment or an explicit else.

_render_meta_html receives resolved start_date/end_date: If the above shadowing bug is fixed (validation moved before resolution), the dates passed to _render_meta_html will correctly reflect what was actually used in the query. Currently they always reflect resolved values, which is fine for display but should be documented.

test_open_end_only_start_date doesn't validate the end date: The test asserts resp.json()["from"] == "2024-01-01" but never checks "to". Since end_date defaults to yesterday (dynamic), the test can't check the exact value — but it could at least verify the field is present and is a valid ISO date.


3. API, Data, and Workflow Safety

Backend/frontend contract: The response schema is unchanged (the PR correctly only modifies the count of records returned, not their shape). No frontend changes are needed and none are made. ✅

resolve_date_range signature change: The function now accepts start_date and end_date kwargs. Any other callers of resolve_date_range that pass positional arguments need to be checked. The diff doesn't show the full function signature, only the body. If the original signature was resolve_date_range(days), adding keyword-only optional params is backward compatible. ✅ (assuming the signature is def resolve_date_range(days, *, start_date=None, end_date=None) or similar).

No database-level filtering shown: The PR passes start_date/end_date to load_meta_timeseries_range which presumably already accepted those parameters. The issue requirement to "push filtering to the data layer" is satisfied only if that function actually uses them in the query rather than fetching all rows and slicing in Python. The diff doesn't show load_meta_timeseries_range's implementation — this should be verified before merging. If that function is already correct (it accepted these args before this PR), a comment noting this assumption would help reviewers.

No CI/deployment impact: No workflow files, CDK infrastructure, or build scripts are changed. ✅


4. Test Coverage

Coverage is generally strong. Specific observations:

test_explicit_start_and_end_date (line ~185): Asserts data["from"] and data["to"] match the input dates — but this only validates the metadata returned in the JSON envelope, not that the underlying DataFrame was actually filtered. The mock patches the loader to return a fixed 3-row DataFrame regardless of the date arguments. Combined with test_load_called_with_resolved_dates, the intent is covered, but the two tests don't combine into a true integration assertion.

The shadowing bug from section 2 is not caught by any test: There's no test for start_date supplied as a future date with no end_date, which would expose the incorrect 422. Add:

def test_future_start_date_only_does_not_raise_422(monkeypatch):
    """open-end range with start in the future should not 422 before resolution."""

test_days_zero_with_end_date_means_all_history: Tests the days=0 + end_date sentinel path cleanly. ✅

test_explicit_start_date_ignores_days: Tests the start_date + days=0 path, verifying the 1900 sentinel is not used. ✅ Good edge case coverage.

Missing test: No test for start_date=2024-01-01&end_date=2024-01-01 through the JSON path (single-day range) to verify data["from"] == data["to"]. The test test_single_day_range exists ✅ but only checks status 200 and the JSON fields — it doesn't verify the loader was called with matching dates.


5. Minor Issues

  • import html is removed from timeseries_meta.py. The _render_meta_html helper no longer calls html.escape() directly — instead it delegates to render_timeseries_html. This is a behavioural change (XSS escaping now depends on whether render_timeseries_html escapes internally). This should be explicitly verified or noted in the PR description.

  • The docstring addition to resolve_date_range is good, but "calendar days back" vs inclusive window (days+1 days) is a subtle distinction that will confuse future callers. Consider a concrete example in the docstring — one is already there (days=2, end_date=2024-01-03 → [2024-01-01, 2024-01-03]) ✅, but it's buried in the docstring body rather than in the Parameters/Returns section.

  • Blank line normalisation in existing tests (removing/adding blank lines around imports) is noise in the diff. Not harmful, just makes the diff harder to read.


Summary

REQUEST CHANGES — The 422 validation guard fires on resolved dates rather than raw user inputs, meaning a valid open-ended request with only start_date supplied can incorrectly return 422 when today is before the supplied start date; this directly contradicts an explicit acceptance criterion. Additionally, the data-layer filtering helper required by the issue was not extracted, which is a design gap that will cause duplication if other endpoints adopt this pattern.


Reviewed by Claude via claude-pr-review.yml. Advisory only.

@leonarduk

Copy link
Copy Markdown
Owner Author

PR Merge Checker — Summary

Merge readiness

Gate Status Notes
Issue linkage Closes #2747 present in PR body
Issue coverage All core ACs from #2747 addressed; apply_date_range helper extraction tracked in #3141
Review comments One open Codex bot comment (#discussion_r3241429461) is on outdated commit c9ea53a — the days window off-by-one it flagged was fixed in commit 5c0e504
CI 2 checks still running (Backend Integration Tests, CI / test); 13/15 passing
Logic correctness Diff is consistent with issue requirements

Verification notes

  • XSS safe: render_timeseries_html in backend/utils/html_render.py calls html.escape(title) and html.escape(subtitle) internally — removing import html from the route is safe.
  • Data-layer filtering confirmed: load_meta_timeseries_range already accepted start_date/end_date keyword arguments on main and passes them to the cache query layer. The PR correctly exposes these via query params.
  • Days window: The explicit_end flag in resolve_date_range correctly anchors start_date relative to the explicit end_date rather than today, preserving the original window size semantics. The Codex off-by-one comment is resolved.
  • Test coverage: Spy-based tests (test_load_called_with_resolved_dates, test_end_date_with_days_derives_start, test_days_zero_with_end_date_means_all_history) verify the resolved dates are actually passed to the loader — this satisfies the core filtering AC.
  • apply_date_range helper: Not implemented in this PR, which is acceptable — follow-up issue Extract apply_date_range helper to service/repository layer #3141 tracks the extraction. The route calls resolve_date_range (in the utils layer) and the data access interface already exists, so the domain logic is not fully in the route.

Actions taken

  • Reviewed all open review comments; the one open Codex inline comment was confirmed addressed by subsequent commits.
  • No code changes needed — the implementation is correct as-is.

Remaining blockers

  • CI: 2 required checks still running. Wait for Backend Integration Tests and CI / test to complete and pass before merging.

Verdict

NOT READY YET — waiting on 2 CI checks to complete. Once those go green, this PR is safe to merge.

@leonarduk leonarduk merged commit 037fcda into main May 28, 2026
15 checks passed
@leonarduk leonarduk deleted the feat/issue-2747-date-range-query branch May 28, 2026 22:40
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.

Allow query by date range not just getting all dates

1 participant