Skip to content

Conversation

@R5dan
Copy link
Contributor

@R5dan R5dan commented May 8, 2025

Summary by CodeRabbit

  • New Features

    • Added support for NASDAQ, SSE (Shanghai Stock Exchange), and LSE (London Stock Exchange) markets with dynamic holiday and trading day data fetching.
    • Introduced database-backed caching for holiday and trading day information, enhancing data persistence.
    • Added new Chinese holiday classes with live holiday date scraping.
    • Provided utility functions for iterating over dates, flattening lists, and managing database connections.
  • Improvements

    • Enhanced market classes with timezone-aware trading hours and refined holiday handling.
    • Standardized type annotations and formatting across modules for clarity.
    • Improved flexibility and robustness in holiday and trading day computations.
  • Bug Fixes

    • Fixed NYSE market logic for abnormal day detection and weekday checks.
  • Chores

    • Added new dependency: platformdirs for cache directory management.
  • Tests

    • Added a test script showcasing holiday retrieval for SSE market.

@coderabbitai
Copy link

coderabbitai bot commented May 8, 2025

Walkthrough

This update introduces significant enhancements to the BetterHolidays package. It adds support for new markets (NASDAQ, SSE, LSE), implements database-backed caching via Peewee ORM, and enriches holiday handling with dynamic date fetching and new utility functions. The Market class is refactored for extensibility, and new modules and dependencies are included to support these features.

Changes

File(s) Change Summary
BetterHolidays/init.py, BetterHolidays/markets/init.py Expanded public API and exports to include NASDAQ, SSE, and LSE markets. Updated MARKETS dictionary and __all__ lists accordingly.
BetterHolidays/const.py Reformatted constant declarations for days, months, and related types; no functional changes.
BetterHolidays/days.py Refactored from frozen dataclasses to Peewee ORM models for all day-related classes, enabling database persistence.
BetterHolidays/markets/cache.py Refactored the Cache class to use a database backend via Peewee ORM, replacing the in-memory cache. Adjusted all cache operations to use database queries.
BetterHolidays/markets/holidays.py Added new utility functions for date calculations, dynamic Chinese holiday fetching, and improved holiday class flexibility and robustness. Introduced new holiday classes and enhanced type annotations.
BetterHolidays/markets/lse.py New module defining the LSE (London Stock Exchange) market class, with holiday scraping and trading day logic.
BetterHolidays/markets/market.py Enhanced Market base class with additional attributes, concrete implementations for data fetching, and new classification methods. Improved type annotations and caching logic.
BetterHolidays/markets/nasdaq.py New module defining the NASDAQ market class, with holiday and trading day logic, web scraping for current-year data, and timezone handling.
BetterHolidays/markets/nyse.py Refactored for clarity and correctness: improved timezone handling, code formatting, and streamlined holiday/trading day logic.
BetterHolidays/markets/sse.py New module defining the SSE (Shanghai Stock Exchange) market class, with Chinese holiday definitions and trading hours.
BetterHolidays/multi.py Updated type annotations in get_market overloads to use double quotes for forward references. Minor whitespace adjustments.
BetterHolidays/utils.py Added database connection utilities, new helper functions (iter_year, flatten_list, default), and improved typing and formatting. Integrated platformdirs and peewee for caching.
requirements.txt, setup.py Added platformdirs>=4.0.0 as a new dependency.
test.py New test script demonstrating the SSE market holidays retrieval for 2020.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Market
    participant Cache
    participant Database
    participant HolidayScraper

    User->>Market: day(date)
    Market->>Cache: get(date)
    Cache->>Database: Query Day by date & market
    alt Found in DB
        Database-->>Cache: Return Day
        Cache-->>Market: Return Day
    else Not found
        Cache-->>Market: Raise exception
        Market->>Market: fetch_data(year)
        Market->>HolidayScraper: (if needed) Scrape holiday data
        HolidayScraper-->>Market: Return holiday dates
        Market->>Cache: set(Day)
        Cache->>Database: Save Day
        Market->>Cache: get(date)
        Cache->>Database: Query Day by date & market
        Database-->>Cache: Return Day
        Cache-->>Market: Return Day
    end
    Market-->>User: Return Day
Loading

Possibly related PRs

  • Basic Implimentation #1: Initial PR that added core day classes and constants, including NonTradingDay, related to module exports and foundational data structures.

Poem

🐇
New markets hop into the fold,
With holidays, both fresh and old.
Days now live in a database burrow,
Scraping calendars for tomorrow.
NASDAQ, SSE, LSE—what a crew!
BetterHolidays, now smarter too.
Hooray for code that’s bright and new!

Tip

⚡️ Faster reviews with caching
  • CodeRabbit now supports caching for code and dependencies, helping speed up reviews. This means quicker feedback, reduced wait times, and a smoother review experience overall. Cached data is encrypted and stored securely. This feature will be automatically enabled for all accounts on May 16th. To opt out, configure Review - Disable Cache at either the organization or repository level. If you prefer to disable all data retention across your organization, simply turn off the Data Retention setting under your Organization Settings.

Enjoy the performance boost—your workflow just got faster.

✨ Finishing Touches
  • 📝 Generate Docstrings

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

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 19

🔭 Outside diff range comments (2)
BetterHolidays/markets/lse.py (1)

27-57: 🛠️ Refactor suggestion

Improve exception handling in fetch_data method.

The exception handling should use raise ... from e to properly chain exceptions. This helps distinguish the original error from the new one.

        except Exception as e:
-            raise ValueError(f"Error fetching LSE data for {year}: {e}")
+            raise ValueError(f"Error fetching LSE data for {year}: {e}") from e

Also, consider adding a docstring to the method explaining its purpose and return value.

🧰 Tools
🪛 Ruff (0.8.2)

54-54: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

BetterHolidays/markets/cache.py (1)

67-69: ⚠️ Potential issue

__contains__ uses in on a model instance – logic error

key in Day.select().where(...) will raise TypeError. Use query.exists().

-        return key in Day.select().where(Day.market == self.market, Day.date == key).get()
+        return (
+            Day.select()
+            .where(Day.market == self.market, Day.date == key)
+            .exists()
+        )
🧹 Nitpick comments (11)
BetterHolidays/markets/__init__.py (2)

8-8: Expand MARKETS registry.
Including the new markets in the MARKETS dict ensures dynamic access by name. Consider alphabetizing keys for easier maintenance.

Example:

- MARKETS: dict[str, type[Market]] = {"NYSE": NYSE, "NASDAQ": NASDAQ, "SSE": SSE, "LSE": LSE}
+ MARKETS: dict[str, type[Market]] = {
+     "LSE": LSE,
+     "NASDAQ": NASDAQ,
+     "NYSE": NYSE,
+     "SSE": SSE,
+ }

10-10: Review __all__ ordering and consistency.
The __all__ list exports all market classes including the new ones. For readability, sorting entries alphabetically can help future contributors.

test.py (1)

1-20: Good demonstration of the classproperty functionality.

This test file effectively demonstrates the getter, setter, and deleter functionality of the classproperty decorator. It showcases how to define a class property, modify its value, and handle deletion.

However, I would recommend adding a docstring to explain the purpose of this test file and consider moving it to a proper test directory if this is intended to be part of the test suite.

BetterHolidays/markets/lse.py (1)

10-22: Market class definition looks good, but could use a docstring.

The LSE class definition with its attributes and regex pattern is well-structured, but adding a docstring would help explain the purpose and behavior of this class.

class LSE(Market):
+    """
+    London Stock Exchange (LSE) market implementation.
+    
+    This class handles trading days and holidays for the London Stock Exchange.
+    """
    name = "London Stock Exchange"
    country = "UK"
    include_country_holidays = True
    excluded_country_holidays = []
BetterHolidays/markets/nasdaq.py (2)

106-108: Drop the redundant f-prefix

The formatted-string literal has no placeholders; Ruff flags this (F541).

-        page = md.HTML.from_url(
-            f"https://www.nasdaq.com/market-activity/stock-market-holiday-schedule"
-        )
+        page = md.HTML.from_url(
+            "https://www.nasdaq.com/market-activity/stock-market-holiday-schedule"
+        )
🧰 Tools
🪛 Ruff (0.8.2)

107-107: f-string without any placeholders

Remove extraneous f prefix

(F541)


35-35: abnormal_days is declared but never populated or consumed

If this dict is meant for run-time overrides it needs to be integrated (e.g. checked in day() lookup) otherwise it is dead code. Consider deleting it until it is actually required.

BetterHolidays/utils.py (1)

86-96: Avoid single-letter variable names and E741 (“ambiguous ‘l’”)

Rename l to items (or similar) to improve readability and silence the Ruff warning.

-def flatten_list(l:'list[list[T]|T1]', recursive:bool=False) -> 'list[T1|T]':
-    ret = []
-    for item in l:
+def flatten_list(items: 'list[list[T] | T1]', recursive: bool = False) -> 'list[T1 | T]':
+    ret: list[T | T1] = []
+    for item in items:
🧰 Tools
🪛 Ruff (0.8.2)

86-86: Ambiguous variable name: l

(E741)

BetterHolidays/markets/market.py (1)

124-150: Minor simplification: return boolean expressions directly

You can return the isinstance result instead of using an if/return pair.
This removes three lines of boilerplate per method.

-        day = cls.day(date)
-        if not isinstance(day, Holiday):
-            return False
-        return True
+        return isinstance(cls.day(date), Holiday)

Same applies to is_partial_day and is_trading_day.

🧰 Tools
🪛 Ruff (0.8.2)

134-136: Return the condition isinstance(day, Holiday) directly

Replace with return isinstance(day, Holiday)

(SIM103)


141-143: Return the condition isinstance(day, PartialTradingDay) directly

Replace with return isinstance(day, PartialTradingDay)

(SIM103)


148-150: Return the condition isinstance(day, TradingDay) directly

Replace with return isinstance(day, TradingDay)

(SIM103)

BetterHolidays/markets/holidays.py (3)

3-3: Remove unused import default.

The symbol default is imported but never referenced in this module, which triggers Ruff F401 and clutters the namespace.

-from ..utils import flatten_list, default
+from ..utils import flatten_list
🧰 Tools
🪛 Ruff (0.8.2)

3-3: ..utils.default imported but unused

Remove unused import: ..utils.default

(F401)


94-98: Combine nested if statements for clarity (Ruff SIM102).

-            if day.weekday() not in self.days:
-                if day.weekday() in self.change:
-                    day += dt.timedelta(days=self.change[day.weekday()])
+            if day.weekday() not in self.days and day.weekday() in self.change:
+                day += dt.timedelta(days=self.change[day.weekday()])
🧰 Tools
🪛 Ruff (0.8.2)

95-96: Use a single if statement instead of nested if statements

(SIM102)


298-301: Preserve exception context when re-raising (B904).

Re-raising with from err keeps the original traceback and avoids masking root causes:

-            return dt.date(year, MONTHS_MAP[month.upper()], int(day))
-        except Exception as e:
-            raise ValueError(
-                f"Error determining Good Friday date for {year}: {str(e)} ({type(e)})"
-            )
+            return dt.date(year, MONTHS_MAP[month.upper()], int(day))
+        except Exception as err:
+            raise ValueError(
+                f"Error determining Good Friday date for {year}: {err} ({type(err).__name__})"
+            ) from err
🧰 Tools
🪛 Ruff (0.8.2)

299-301: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f57e4b9 and 93d553b.

📒 Files selected for processing (16)
  • BetterHolidays/__init__.py (2 hunks)
  • BetterHolidays/const.py (3 hunks)
  • BetterHolidays/days.py (1 hunks)
  • BetterHolidays/markets/__init__.py (1 hunks)
  • BetterHolidays/markets/cache.py (1 hunks)
  • BetterHolidays/markets/holidays.py (3 hunks)
  • BetterHolidays/markets/lse.py (1 hunks)
  • BetterHolidays/markets/market.py (2 hunks)
  • BetterHolidays/markets/nasdaq.py (1 hunks)
  • BetterHolidays/markets/nyse.py (1 hunks)
  • BetterHolidays/markets/sse.py (1 hunks)
  • BetterHolidays/multi.py (1 hunks)
  • BetterHolidays/utils.py (2 hunks)
  • requirements.txt (1 hunks)
  • setup.py (1 hunks)
  • test.py (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (4)
test.py (1)
BetterHolidays/utils.py (2)
  • classproperty (44-65)
  • delete (54-56)
BetterHolidays/days.py (1)
BetterHolidays/utils.py (1)
  • get_db (40-41)
BetterHolidays/utils.py (1)
BetterHolidays/markets/market.py (1)
  • days (164-165)
BetterHolidays/markets/market.py (7)
BetterHolidays/days.py (5)
  • Day (5-12)
  • Holiday (14-17)
  • TradingDay (20-24)
  • PartialTradingDay (33-39)
  • NonTradingDay (27-30)
BetterHolidays/utils.py (3)
  • iterate_date (75-79)
  • iter_year (81-84)
  • default (98-99)
BetterHolidays/markets/cache.py (2)
  • Cache (12-68)
  • get_or_set (23-32)
BetterHolidays/markets/lse.py (2)
  • fetch_data (28-56)
  • weekdays (24-25)
BetterHolidays/markets/nasdaq.py (2)
  • fetch_data (94-98)
  • weekdays (90-91)
BetterHolidays/markets/nyse.py (2)
  • fetch_data (184-188)
  • weekdays (180-181)
BetterHolidays/markets/sse.py (1)
  • weekdays (34-35)
🪛 Ruff (0.8.2)
BetterHolidays/markets/lse.py

23-23: Undefined name classproperty

(F821)


54-54: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

BetterHolidays/markets/cache.py

32-32: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


52-52: Do not use bare except

(E722)


53-53: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


59-59: Do not use bare except

(E722)


64-64: Local variable e is assigned to but never used

Remove assignment to unused variable e

(F841)


65-65: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

BetterHolidays/markets/nasdaq.py

10-10: .holidays.LincolnsBirthday imported but unused

Remove unused import: .holidays.LincolnsBirthday

(F401)


107-107: f-string without any placeholders

Remove extraneous f prefix

(F541)


121-121: Undefined name year

(F821)


148-148: Undefined name year

(F821)

BetterHolidays/markets/sse.py

3-3: ..days.Holiday imported but unused

Remove unused import: ..days.Holiday

(F401)


4-4: ..const.MONTHS_MAP imported but unused

Remove unused import: ..const.MONTHS_MAP

(F401)


5-5: BetterMD imported but unused

Remove unused import: BetterMD

(F401)


33-33: Undefined name classproperty

(F821)

BetterHolidays/utils.py

86-86: Ambiguous variable name: l

(E741)

BetterHolidays/markets/market.py

134-136: Return the condition isinstance(day, Holiday) directly

Replace with return isinstance(day, Holiday)

(SIM103)


141-143: Return the condition isinstance(day, PartialTradingDay) directly

Replace with return isinstance(day, PartialTradingDay)

(SIM103)


148-150: Return the condition isinstance(day, TradingDay) directly

Replace with return isinstance(day, TradingDay)

(SIM103)

BetterHolidays/markets/holidays.py

3-3: ..utils.default imported but unused

Remove unused import: ..utils.default

(F401)


95-96: Use a single if statement instead of nested if statements

(SIM102)


186-186: Undefined name date

(F821)


293-293: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


299-301: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

BetterHolidays/markets/nyse.py

219-219: Function definition does not bind loop variable year

(B023)

🔇 Additional comments (17)
requirements.txt (1)

1-2:

Details

✅ Verification successful

Verify dependency alignment across files.
The new platformdirs>=4.0.0 entry must match the dependency in setup.py and support your database caching implementation in BetterHolidays/utils.py. Please ensure both files stay in sync to avoid mismatched environments.

Run this script to cross-check versions:


🏁 Script executed:

#!/bin/bash
# Verify presence and versions of key dependencies in both files
grep -E 'platformdirs|better-md' requirements.txt setup.py

Length of output: 208


Dependency versions are in sync
Verified that both requirements.txt and setup.py specify better-md>=0.3.4 and platformdirs>=4.0.0. No mismatches found—no further action needed.

setup.py (1)

18-21:

Details

✅ Verification successful

Ensure install_requires matches requirements.txt.
You've added "platformdirs>=4.0.0" under install_requires. Confirm that this lines up exactly with your requirements.txt entry and that no other dependencies are omitted.

Use the following command to verify:


🏁 Script executed:

#!/bin/bash
grep -E 'platformdirs|better-md' requirements.txt setup.py

Length of output: 208


install_requires matches requirements.txt
All dependencies (better-md>=0.3.4, platformdirs>=4.0.0) are identical in both setup.py and requirements.txt.

BetterHolidays/const.py (5)

3-3: Formatting update for DAYS tuple.
Consolidating the weekdays into a single-line tuple improves readability without affecting functionality.


6-8: Standardize day constant declarations.
The assignments for MONDAYSUNDAY and the DAYS_TYPE annotation have been reformatted for consistency. Behavior remains unchanged.


21-21: Add trailing comma for multi-line tuple.
Including a trailing comma after "DECEMBER" in MONTHS is a good practice for cleaner diffs and future edits.


25-38: Reformat month constants assignment.
Switching to a multi-line unpacking of range(1, 13) maintains the same values while enhancing visual structure.


53-53: Add trailing comma to dictionary entry.
Adding a trailing comma after the last DAYS_IN_MONTH entry keeps the style consistent and eases future modifications.

BetterHolidays/__init__.py (2)

3-3: Expose new markets in package-level imports.
Importing NASDAQ, SSE, and LSE alongside existing markets ensures they are available through the top-level API. No circular import issues detected.


13-18: Update __all__ to include additional markets.
The public API now correctly exposes NASDAQ, SSE, LSE, and get_market. The trailing comma and newline addition align with PEP-8 recommendations.

BetterHolidays/markets/__init__.py (1)

4-6:

Details

✅ Verification successful

Import new market classes.
Adding NASDAQ, SSE, and LSE here aligns with the new submodules. Ensure that nasdaq.py, sse.py, and lse.py define these classes exactly.

Run:


🏁 Script executed:

#!/bin/bash
grep -R "class NASDAQ" -n BetterHolidays/markets
grep -R "class SSE"   -n BetterHolidays/markets
grep -R "class LSE"   -n BetterHolidays/markets

Length of output: 308


Imports Verified: NASDAQ, SSE, and LSE classes are present in their modules
grep confirms:

  • BetterHolidays/markets/nasdaq.py:24 ⇒ class NASDAQ(Market):
  • BetterHolidays/markets/sse.py:10 ⇒ class SSE(Market):
  • BetterHolidays/markets/lse.py:10 ⇒ class LSE(Market):

No further changes needed here.

BetterHolidays/multi.py (2)

9-16: Consistent type annotation style improvement.

The changes to the type annotations, using double quotes instead of single quotes for forward references, improve consistency with the rest of the codebase. These types of style standardizations make the code more maintainable.


21-25: Whitespace improvements for better readability.

Adding blank lines between logical sections of the code improves readability and follows common Python style guidelines.

BetterHolidays/markets/lse.py (1)

23-26: Classproperty usage is correct, pending import fix.

The implementation of the weekdays property is correct, returning the days Monday to Friday (0-4).

🧰 Tools
🪛 Ruff (0.8.2)

23-23: Undefined name classproperty

(F821)

BetterHolidays/markets/sse.py (4)

16-20: Timezone definition looks good.

The timezone definition and trading hours are well-defined using the zoneinfo module.


23-31: Holiday definitions are comprehensive.

The holiday definitions for the Shanghai Stock Exchange are comprehensive, covering major Chinese holidays with appropriate weekday adjustments.


33-35: Classproperty implementation is correct, pending import fix.

The weekdays property correctly indicates Monday to Friday as trading days.

🧰 Tools
🪛 Ruff (0.8.2)

33-33: Undefined name classproperty

(F821)


1-35:

Details

❌ Incorrect review comment

File appears incomplete.

The file seems to be missing implementation for fetching or processing holiday data, unlike the LSE implementation. Check if this is intentional or if the implementation relies on the holiday classes to handle this.


🏁 Script executed:

#!/bin/bash
# Check if there's a missing fetch_data method or if the holiday classes handle the data fetching
echo "Checking for fetch_data implementation in parent class:"
rg -A 5 "def fetch_data" BetterHolidays/markets/market.py

echo -e "\nChecking how holiday classes fetch data:"
rg -A 5 "def get_date|def get_dates" BetterHolidays/markets/holidays.py

Length of output: 4597


No missing implementation in SSE: holiday classes provide the data fetching
The SSE class inherits fetch_data from Market, which in turn calls each holiday’s get_date implementation. All holiday classes (e.g. ChineseNewYearsDay, QingMingFestival, etc.) define get_date by fetching or computing their dates, so no additional methods are required in SSE.
No changes needed.

Likely an incorrect or invalid review comment.

🧰 Tools
🪛 Ruff (0.8.2)

3-3: ..days.Holiday imported but unused

Remove unused import: ..days.Holiday

(F401)


4-4: ..const.MONTHS_MAP imported but unused

Remove unused import: ..const.MONTHS_MAP

(F401)


5-5: BetterMD imported but unused

Remove unused import: BetterMD

(F401)


33-33: Undefined name classproperty

(F821)

Comment on lines 24 to 34
def last_day(day: "DAYS_TYPE", want: "DAYS_TYPE") -> "DAYS_TYPE":
"""
Args:
day: current day
want: day to get
Returns:
days until want
"""
return next_day(day, want)

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

last_day() returns the wrong offset (always “forward” instead of “back”)

last_day() should give the distance to the previous occurrence of want, but the current implementation simply delegates to next_day(), producing an offset in the wrong direction (e.g. from Wednesday to Sunday it returns 4 instead of 3).
This mis-calculates many holiday ranges (e.g. Chinese New Year’s Eve) and makes last_weekend() behave erratically.

-def last_day(day: "DAYS_TYPE", want: "DAYS_TYPE") -> "DAYS_TYPE":
-    """
-    Args:
-        day: current day
-        want: day to get
-    
-    Returns:
-        days until want
-    """
-    return next_day(day, want)
+def last_day(day: "DAYS_TYPE", want: "DAYS_TYPE") -> "DAYS_TYPE":
+    """
+    Days to step *backwards* from ``day`` to the previous ``want``.
+    Always non-negative so it can safely be used in ``datetime.timedelta``.
+    """
+    if day == want:
+        return 0
+    if want > day:
+        # Wrap backwards over the week boundary
+        return (day + 7) - want
+    # Same week, simply walk back
+    return day - want
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def last_day(day: "DAYS_TYPE", want: "DAYS_TYPE") -> "DAYS_TYPE":
"""
Args:
day: current day
want: day to get
Returns:
days until want
"""
return next_day(day, want)
def last_day(day: "DAYS_TYPE", want: "DAYS_TYPE") -> "DAYS_TYPE":
"""
Days to step *backwards* from ``day`` to the previous ``want``.
Always non-negative so it can safely be used in ``datetime.timedelta``.
"""
if day == want:
return 0
if want > day:
# Wrap backwards over the week boundary
return (day + 7) - want
# Same week, simply walk back
return day - want

Comment on lines 47 to 57
def last_weekend(day: "DAYS_TYPE") -> "DAYS_TYPE":
"""
Args:
day: current day
Returns:
days until want
"""
if day in [5,6]:
return min(last_day(day-1, 5), last_day(day-1, 6)) - 1
return last_day(day, 6)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

last_weekend() off-by-one and negative offsets

Current logic may return negative numbers (e.g. ‑1 for Sunday) or an excessive offset (e.g. 4 from Wednesday → Saturday).
A safer implementation that always walks back to the most recent Sunday:

-def last_weekend(day: "DAYS_TYPE") -> "DAYS_TYPE":
-    ...
-    if day in [5,6]:
-        return min(last_day(day-1, 5), last_day(day-1, 6)) - 1
-    return last_day(day, 6)
+def last_weekend(day: "DAYS_TYPE") -> "DAYS_TYPE":
+    """
+    Days to the most recent Sunday (weekday 6).
+    Returns 0 when already on a weekend day.
+    """
+    if day == 6:              # Sunday
+        return 0
+    if day == 5:              # Saturday
+        return 1
+    return day + 1            # Monday-Friday
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def last_weekend(day: "DAYS_TYPE") -> "DAYS_TYPE":
"""
Args:
day: current day
Returns:
days until want
"""
if day in [5,6]:
return min(last_day(day-1, 5), last_day(day-1, 6)) - 1
return last_day(day, 6)
def last_weekend(day: "DAYS_TYPE") -> "DAYS_TYPE":
"""
Days to the most recent Sunday (weekday 6).
Returns 0 when already on a weekend day.
"""
if day == 6: # Sunday
return 0
if day == 5: # Saturday
return 1
return day + 1 # Monday–Friday

Comment on lines 35 to 46
def next_weekend(day: "DAYS_TYPE") -> "DAYS_TYPE":
"""
Args:
day: current day
Returns:
days until want
"""
if day in [5,6]:
return min(next_day(day+1, 5), next_day(day+1, 6)) + 1
return next_day(day, 5)

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

next_weekend() can yield invalid weekday values (e.g. uses 7)

Passing day+1 to next_day() when day is already 6 produces 7, which is outside the Python weekday range 0-6 and causes logic errors.
A simpler, correct formulation:

-def next_weekend(day: "DAYS_TYPE") -> "DAYS_TYPE":
-    ...
-    if day in [5,6]:
-        return min(next_day(day+1, 5), next_day(day+1, 6)) + 1
-    return next_day(day, 5)
+def next_weekend(day: "DAYS_TYPE") -> "DAYS_TYPE":
+    """
+    Days until the upcoming Saturday (weekday 5).
+    Returns 0 when called on a weekend day.
+    """
+    if day <= 4:        # Monday-Friday
+        return 5 - day
+    return 0            # Already weekend
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def next_weekend(day: "DAYS_TYPE") -> "DAYS_TYPE":
"""
Args:
day: current day
Returns:
days until want
"""
if day in [5,6]:
return min(next_day(day+1, 5), next_day(day+1, 6)) + 1
return next_day(day, 5)
def next_weekend(day: "DAYS_TYPE") -> "DAYS_TYPE":
"""
Days until the upcoming Saturday (weekday 5).
Returns 0 when called on a weekend day.
"""
if day <= 4: # Monday–Friday
return 5 - day
return 0 # Already weekend

@R5dan
Copy link
Contributor Author

R5dan commented May 8, 2025

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented May 8, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🔭 Outside diff range comments (1)
BetterHolidays/markets/nasdaq.py (1)

186-187: ⚠️ Potential issue

Remove debug print statements

These print statements were likely added for debugging and should be removed before merging.

-        print(page)
-        print(len(page))
♻️ Duplicate comments (3)
BetterHolidays/markets/holidays.py (3)

50-60: ⚠️ Potential issue

Fix last_weekend() to handle edge cases correctly

The current implementation has off-by-one errors and can return negative offsets, which might cause unexpected behavior when used in date calculations.

-def last_weekend(day: "DAYS_TYPE") -> "DAYS_TYPE":
-    """
-    Args:
-        day: current day
-
-    Returns:
-        days until want
-    """
-    if day in [5, 6]:
-        return min(last_day(day - 1, 5), last_day(day - 1, 6)) - 1
-    return last_day(day, 6)
+def last_weekend(day: "DAYS_TYPE") -> "DAYS_TYPE":
+    """
+    Days to the most recent Sunday (weekday 6).
+    Returns 0 when already on a weekend day.
+    
+    Args:
+        day: current day
+
+    Returns:
+        days to previous weekend
+    """
+    if day == 6:              # Sunday
+        return 0
+    if day == 5:              # Saturday
+        return 1
+    return day + 1            # Monday-Friday

25-34: ⚠️ Potential issue

Fix last_day() to actually go backwards in time

Currently, last_day() incorrectly delegates to next_day(), which calculates in the wrong direction. This likely mis-calculates many holiday ranges, especially for the Chinese markets.

-def last_day(day: "DAYS_TYPE", want: "DAYS_TYPE") -> "DAYS_TYPE":
-    """
-    Args:
-        day: current day
-        want: day to get
-
-    Returns:
-        days until want
-    """
-    return next_day(day, want)
+def last_day(day: "DAYS_TYPE", want: "DAYS_TYPE") -> "DAYS_TYPE":
+    """
+    Days to step *backwards* from ``day`` to the previous ``want``.
+    Always non-negative so it can safely be used in ``datetime.timedelta``.
+    
+    Args:
+        day: current day
+        want: day to get
+
+    Returns:
+        days to previous occurrence of want
+    """
+    if day == want:
+        return 0
+    if want > day:
+        # Wrap backwards over the week boundary
+        return (day + 7) - want
+    # Same week, simply walk back
+    return day - want

37-47: ⚠️ Potential issue

Fix next_weekend() to handle edge cases correctly

The current implementation can yield invalid weekday values (e.g., day 7 when day is already 6), causing logic errors.

-def next_weekend(day: "DAYS_TYPE") -> "DAYS_TYPE":
-    """
-    Args:
-        day: current day
-
-    Returns:
-        days until want
-    """
-    if day in [5, 6]:
-        return min(next_day(day + 1, 5), next_day(day + 1, 6)) + 1
-    return next_day(day, 5)
+def next_weekend(day: "DAYS_TYPE") -> "DAYS_TYPE":
+    """
+    Days until the upcoming Saturday (weekday 5).
+    Returns 0 when called on a weekend day.
+    
+    Args:
+        day: current day
+
+    Returns:
+        days until weekend
+    """
+    if day <= 4:        # Monday-Friday
+        return 5 - day
+    return 0            # Already weekend
🧹 Nitpick comments (12)
BetterHolidays/utils.py (1)

87-97: Replace ambiguous variable name 'l'

Using 'l' as a variable name is ambiguous as it can be confused with the number '1' in some fonts.

-def flatten_list(l: "list[list[T]|T1]", recursive: bool = False) -> "list[T1|T]":
+def flatten_list(lst: "list[list[T]|T1]", recursive: bool = False) -> "list[T1|T]":
    ret = []
-    for item in l:
+    for item in lst:
        if isinstance(item, list):
            if recursive:
                ret.extend(flatten_list(item, recursive))
            else:
                ret.extend(item)
        else:
            ret.append(item)
    return ret
🧰 Tools
🪛 Ruff (0.8.2)

87-87: Ambiguous variable name: l

(E741)

BetterHolidays/markets/lse.py (1)

53-54: Improve exception handling with "from" clause

Current exception handling obscures the original error's traceback. Use from e to preserve it or from None to suppress it.

-        raise ValueError(f"Error fetching LSE data for {year}: {e}")
+        raise ValueError(f"Error fetching LSE data for {year}: {e}") from e
🧰 Tools
🪛 Ruff (0.8.2)

54-54: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

BetterHolidays/markets/cache.py (2)

17-19: Remove debug print statement

The print(selection) statement appears to be debugging code that should be removed before production deployment.

    def get(self, key: "dt.date") -> "Day":
        selection = Day.select().where(Day.date == key, Day.market == self.market)
-        print(selection)
        return selection.get()

33-34: Improve exception handling with "from" clause

Use the from None syntax to clarify that this is a new exception rather than a chained one.

-            raise ValueError(f"Could not find day {key} after fetching data")
+            raise ValueError(f"Could not find day {key} after fetching data") from None
🧰 Tools
🪛 Ruff (0.8.2)

34-34: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

BetterHolidays/days.py (1)

39-39: Consider moving table creation to an initialization function

Creating tables directly at module level could cause issues if the module is imported multiple times or if circular imports exist. Consider moving this to an initialization function.

-utils.get_db().create_tables([Day, Holiday, TradingDay, NonTradingDay, PartialTradingDay])
+def initialize_tables():
+    utils.get_db().create_tables([Day, Holiday, TradingDay, NonTradingDay, PartialTradingDay])
+
+# Call the function at import time if needed
+initialize_tables()
BetterHolidays/markets/nasdaq.py (1)

106-107: Remove unnecessary f-string prefix

The URL string does not contain any placeholders, so the f prefix is unnecessary.

-        page = md.HTML.from_url(
-            f"https://www.nasdaq.com/market-activity/stock-market-holiday-schedule"
-        )
+        page = md.HTML.from_url(
+            "https://www.nasdaq.com/market-activity/stock-market-holiday-schedule"
+        )
🧰 Tools
🪛 Ruff (0.8.2)

106-106: f-string without any placeholders

Remove extraneous f prefix

(F541)

BetterHolidays/markets/market.py (3)

152-156: Simplify boolean return statements

The function can be simplified by directly returning the result of the isinstance check.

-    @classmethod
-    def is_holiday(cls, date: "dt.date") -> bool:
-        day = cls.day(date)
-        if not isinstance(day, Holiday):
-            return False
-        return True
+    @classmethod
+    def is_holiday(cls, date: "dt.date") -> bool:
+        day = cls.day(date)
+        return isinstance(day, Holiday)
🧰 Tools
🪛 Ruff (0.8.2)

154-156: Return the condition isinstance(day, Holiday) directly

Replace with return isinstance(day, Holiday)

(SIM103)


159-163: Simplify boolean return statements

The function can be simplified by directly returning the result of the isinstance check.

-    @classmethod
-    def is_partial_day(cls, date: "dt.date") -> "bool":
-        day = cls.day(date)
-        if not isinstance(day, PartialTradingDay):
-            return False
-        return True
+    @classmethod
+    def is_partial_day(cls, date: "dt.date") -> "bool":
+        day = cls.day(date)
+        return isinstance(day, PartialTradingDay)
🧰 Tools
🪛 Ruff (0.8.2)

161-163: Return the condition isinstance(day, PartialTradingDay) directly

Replace with return isinstance(day, PartialTradingDay)

(SIM103)


166-170: Simplify boolean return statements

The function can be simplified by directly returning the result of the isinstance check.

-    @classmethod
-    def is_trading_day(cls, date: "dt.date") -> "bool":
-        day = cls.day(date)
-        if not isinstance(day, TradingDay):
-            return False
-        return True
+    @classmethod
+    def is_trading_day(cls, date: "dt.date") -> "bool":
+        day = cls.day(date)
+        return isinstance(day, TradingDay)
🧰 Tools
🪛 Ruff (0.8.2)

168-170: Return the condition isinstance(day, TradingDay) directly

Replace with return isinstance(day, TradingDay)

(SIM103)

BetterHolidays/markets/holidays.py (3)

353-355: Improve exception handling with proper error chaining

Use raise ... from e syntax to preserve the exception chain for better debugging.

-            except Exception as e:
-                raise ValueError(f"Error finding Good Friday information: {str(e)}")
+            except Exception as e:
+                raise ValueError(f"Error finding Good Friday information: {str(e)}") from e
🧰 Tools
🪛 Ruff (0.8.2)

354-354: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


360-362: Improve exception handling with proper error chaining

Use raise ... from e syntax to preserve the exception chain for better debugging.

-            raise ValueError(
-                f"Error determining Good Friday date for {year}: {str(e)} ({type(e)})"
-            )
+            raise ValueError(
+                f"Error determining Good Friday date for {year}: {str(e)} ({type(e)})"
+            ) from e
🧰 Tools
🪛 Ruff (0.8.2)

360-362: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


3-3: Remove unused import

The default function is imported but never used in this file.

-from ..utils import flatten_list, default
+from ..utils import flatten_list
🧰 Tools
🪛 Ruff (0.8.2)

3-3: ..utils.default imported but unused

Remove unused import: ..utils.default

(F401)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 93d553b and 05d3d30.

📒 Files selected for processing (11)
  • BetterHolidays/days.py (1 hunks)
  • BetterHolidays/markets/__init__.py (1 hunks)
  • BetterHolidays/markets/cache.py (1 hunks)
  • BetterHolidays/markets/holidays.py (3 hunks)
  • BetterHolidays/markets/lse.py (1 hunks)
  • BetterHolidays/markets/market.py (2 hunks)
  • BetterHolidays/markets/nasdaq.py (1 hunks)
  • BetterHolidays/markets/nyse.py (1 hunks)
  • BetterHolidays/markets/sse.py (1 hunks)
  • BetterHolidays/utils.py (2 hunks)
  • test.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • test.py
  • BetterHolidays/markets/init.py
  • BetterHolidays/markets/sse.py
  • BetterHolidays/markets/nyse.py
🧰 Additional context used
🧬 Code Graph Analysis (3)
BetterHolidays/markets/cache.py (3)
BetterHolidays/utils.py (1)
  • get_db (40-41)
BetterHolidays/days.py (1)
  • Day (4-11)
BetterHolidays/markets/market.py (1)
  • day (180-181)
BetterHolidays/days.py (1)
BetterHolidays/utils.py (1)
  • get_db (40-41)
BetterHolidays/utils.py (1)
BetterHolidays/markets/market.py (1)
  • days (184-190)
🪛 Ruff (0.8.2)
BetterHolidays/markets/lse.py

54-54: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

BetterHolidays/markets/cache.py

34-34: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

BetterHolidays/markets/nasdaq.py

106-106: f-string without any placeholders

Remove extraneous f prefix

(F541)

BetterHolidays/markets/market.py

154-156: Return the condition isinstance(day, Holiday) directly

Replace with return isinstance(day, Holiday)

(SIM103)


161-163: Return the condition isinstance(day, PartialTradingDay) directly

Replace with return isinstance(day, PartialTradingDay)

(SIM103)


168-170: Return the condition isinstance(day, TradingDay) directly

Replace with return isinstance(day, TradingDay)

(SIM103)

BetterHolidays/markets/holidays.py

3-3: ..utils.default imported but unused

Remove unused import: ..utils.default

(F401)


98-99: Use a single if statement instead of nested if statements

(SIM102)


354-354: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


360-362: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

BetterHolidays/utils.py

87-87: Ambiguous variable name: l

(E741)

🔇 Additional comments (9)
BetterHolidays/utils.py (3)

17-18: Good fix for cache directory handling

You've correctly fixed the typo in the cache directory name and added directory creation, addressing previous review comments effectively.


34-41: Well-implemented database connection setup

The implementation follows previous review suggestions to ensure the database is properly connected with reuse_if_open=True. This avoids potential race conditions in multi-threaded contexts.


81-84: Good utility function for year iteration

The iter_year function provides a clean, reusable way to iterate through all dates in a given year, which is useful for market day processing.

BetterHolidays/markets/lse.py (2)

1-7: Imports include classproperty as suggested

The code now correctly imports classproperty from utils, addressing the previous review comment.


29-53: Well-structured market data fetching implementation

The implementation cleanly scrapes holiday data from an external calendar website, parses the dates, and properly classifies each day of the year as a Holiday, TradingDay, or NonTradingDay.

BetterHolidays/markets/cache.py (1)

21-23: Fixed property assignment before save

You've properly fixed the issue by directly setting the market property before saving, as recommended in the previous review.

BetterHolidays/days.py (2)

7-8: Database connection now works correctly

Your implementation of get_db() with the @method_cache decorator now correctly handles the database connection, addressing the previous review comment about missing arguments.


31-37: Well-designed PartialTradingDay model

The PartialTradingDay model provides a clean way to represent days with special trading hours, including appropriate fields for early close and late open scenarios with reasons.

BetterHolidays/markets/holidays.py (1)

205-205: ⚠️ Potential issue

Fix undefined name in type annotation

The type annotation uses date which is undefined and could cause errors when annotations are evaluated.

-    def get_date(self, year: int) -> 'dt.date | list[dt.date]':
+    def get_date(self, year: int) -> 't.Union[dt.date, list[dt.date]]':

Likely an incorrect or invalid review comment.

Comment on lines +58 to +61
def __contains__(self, key: "dt.date") -> bool:
return (
key in Day.select().where(Day.market == self.market, Day.date == key).get()
)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix incorrect implementation of contains

The current implementation tries to check if a date is within a Day object, which is incorrect. It should check if the query returns any results.

    def __contains__(self, key: "dt.date") -> bool:
-        return (
-            key in Day.select().where(Day.market == self.market, Day.date == key).get()
-        )
+        try:
+            Day.select().where(Day.market == self.market, Day.date == key).get()
+            return True
+        except pw.DoesNotExist:
+            return False
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def __contains__(self, key: "dt.date") -> bool:
return (
key in Day.select().where(Day.market == self.market, Day.date == key).get()
)
def __contains__(self, key: "dt.date") -> bool:
try:
Day.select().where(Day.market == self.market, Day.date == key).get()
return True
except pw.DoesNotExist:
return False

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant