Skip to content

Client

client

Client for Motley-Fool.

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

Motley-Fool HTTP client with httpx or Selenium mode as currently 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._rate_limiter = RateLimiter(requests_per_minute=self._LIMITS.requests_per_minute)

    self._client: httpx.Client | uc.Chrome | None

    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()

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 not self.supports_identifier_type(source_identifier.type):
            ClientHelperErrorHandler.invalid_security_type(DataSourceID.MOTLEYFOOL, source_identifier)
            continue

        if "-" in source_identifier.value_cleaned:
            source_identifiers_cleaned = [
                SecurityIdentifier(type=source_identifier.type, value=source_identifier.value.replace("-", "")),
                SecurityIdentifier(type=source_identifier.type, value=source_identifier.value.replace("-", "."))
            ]
        else:
            source_identifiers_cleaned = [source_identifier]

        for source_identifier_cleaned in source_identifiers_cleaned:

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

            search_result = self._select_search_result(source_identifier_cleaned, 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:
                    record.identifiers.replace(source_identifier)
                    records.append(record)

    return records

remove_records_without_classification(records: list[MotleyFoolRecord]) -> list[MotleyFoolRecord] staticmethod

Remove records without share_class_FIGI if a ISIN-matching record exists.

Source code in src/equities_classifier/clients/motleyfool/client.py
@staticmethod
def remove_records_without_classification(
    records: list[MotleyFoolRecord],
) -> list[MotleyFoolRecord]:
    """Remove records without share_class_FIGI if a ISIN-matching record exists."""

    count_before = len(records)

    records = [
        record
        for record in records
        if record.sector and record.industry
    ]

    count_after = len(records)

    if count_before - count_after > 0:
        ClientHelperErrorHandler.records_cleaned(
           DataSourceID.MOTLEYFOOL,
           count_before - count_after,
           "records without expected classification data"
        )

    return records

supports_identifier_type(identifier_type: SecurityIdentifierType) -> bool classmethod

Check if identifier type supported

Source code in src/equities_classifier/clients/motleyfool/client.py
@classmethod
def supports_identifier_type(cls, identifier_type: SecurityIdentifierType, ) -> bool:
    """Check if identifier type supported"""
    return identifier_type in cls._MOTLEYFOOL_IDENTIFIER_TYPES.values()

MotleyFoolMode

Supported client modes.

HTTPX = 'httpx' class-attribute instance-attribute

SELENIUM = 'SELENIUM' class-attribute instance-attribute

MotleyFoolResponseError

Motley-Fool returned an invalid response.