Skip to content

Motleyfool

motleyfool

__all__ = ['MotleyFoolClient', 'MotleyFoolRecord'] module-attribute

MotleyFoolClient(timeout: float = 30.0, mode: MotleyFoolMode = MotleyFoolMode.HTTPX, requestlog: bool = False)

Motley-Fool HTTP client with httpx or Selenium mode as currenlty used fallback.

Source code in src/equities_classifier/clients/motleyfool/client.py
def __init__(
    self,
    timeout: float = 30.0,
    mode: MotleyFoolMode = MotleyFoolMode.HTTPX,
    requestlog: bool = False
) -> None:
    """Initialize Motley-Fool client."""

    self._client: httpx.Client | uc.Chrome

    self._mode = mode
    if self._mode == MotleyFoolMode.HTTPX:
        self._client = httpx.Client(
            base_url=self._BASE_URL,
            timeout=timeout,
            follow_redirects=True,
            event_hooks={"request": [log_request], "response": [log_response], } if requestlog else None
        )
        # determine action code for next.js
        self._next_action = self._get_next_action()
    elif self._mode == MotleyFoolMode.SELENIUM:
        options = uc.ChromeOptions()
        options.add_argument("--headless=new")
        options.add_argument("--no-sandbox")
        options.add_argument("--disable-dev-shm-usage")
        try:
            self._client = uc.Chrome(options=options)
        except Exception as e:
            print("Failed to start Chrome")
            raise e
        try:
            config = StabilizationConfig(
                timeout=10,  # Max wait time (seconds)
                network_idle_threshold=5,  # Max pending requests (allows background traffic)
                strictness='normal',  # 'strict' | 'normal' | 'relaxed'
                debug_mode=False  # Enable logging
            )
            self._client = stabilize(self._client, config=config)
        except StabilizationTimeout as e:
            print("Failed to stabilize Chrome  with 'waitless'")
            diagnostics = get_diagnostics(self._client)
            print_report(diagnostics)  # Print detailed report
            raise e
        self._client.get(self._BASE_URL)
        time.sleep(5)  # additional wait for cookie popup (required in GitHub Action environment)
        self._client.find_element(By.XPATH, "//button[@id='onetrust-accept-btn-handler']").click()
    else:
        msg = f"MotleyFoolClient mode '{self._mode}' not valid.)"
        raise MotleyFoolResponseError(msg)

__enter__() -> Self

Source code in src/equities_classifier/clients/motleyfool/client.py
def __enter__(self) -> Self:
    return self

__exit__(*_: object) -> None

Source code in src/equities_classifier/clients/motleyfool/client.py
def __exit__(self, *_: object) -> None:
    self.close()

close() -> None

Release client resources.

Source code in src/equities_classifier/clients/motleyfool/client.py
def close(self) -> None:
    """Release client resources."""

    # check self_client before execution due to potential double-close when using pytest
    if self._client:
        self._client.close()
        self._client = None

read_provider_profile_data(source_identifiers: Sequence[SecurityIdentifier], raise_error: bool = False) -> list[MotleyFoolRecord]

Read profile data for one or more identifiers from Motley-Fool.

Source code in src/equities_classifier/clients/motleyfool/client.py
def read_provider_profile_data(
    self,
    source_identifiers: Sequence[SecurityIdentifier],
    raise_error: bool = False,
) -> list[MotleyFoolRecord]:
    """Read profile data for one or more identifiers from Motley-Fool."""

    records: list[MotleyFoolRecord] = []

    for source_identifier in source_identifiers:

        if source_identifier.type != SecurityIdentifierType.TICKER:
            ClientHelper.invalid_security_type(
                DataSourceID.MOTLEYFOOL,
                source_identifier
            )
            continue

        if self._mode == MotleyFoolMode.HTTPX:
            # determine search result and fill search_result via requests
            response_data = self._execute_search_request(source_identifier)
            search_results = self._parse_search_results(
                source_identifier,
                response_data,
                raise_error,
            )
        else:
            # determine search result and fill search_result via selenium / HTML analysis
            search_results = self._get_search_results(
                source_identifier,
                raise_error,
            )

        search_result = self._select_search_result(
            source_identifier,
            search_results,
            raise_error,
        )
        if search_result:

            if self._mode == MotleyFoolMode.HTTPX:
                html = self._execute_company_request(search_result)
            else:
                html = self._get_company_profile_html(search_result)

            record = self._parse_record(
                search_result,
                html,
                raise_error,
            )
            if record:
                records.append(record)

    return records

MotleyFoolRecord(name: str | None = None, ticker: str | None = None, exchange: str | None = None, home_country_code: str | None = None, sector: str | None = None, industry: str | None = None, *, identifiers: list[SecurityIdentifier] = list()) dataclass

Internal representation of a single Motley-Fool mapping result.

datasource: DataSourceID = DataSourceID.MOTLEYFOOL class-attribute

exchange: str | None = None class-attribute instance-attribute

home_country_code: str | None = None class-attribute instance-attribute

industry: str | None = None class-attribute instance-attribute

sector: str | None = None class-attribute instance-attribute