Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,21 @@ All notable changes to the `sahmk` Python SDK will be documented in this file.

This project follows [Semantic Versioning](https://semver.org/).

## [0.6.1] — 2026-04-02

### Fixed

- **CLI `--compact` flag** now works in both positions: `sahmk --compact quote 2222` and `sahmk quote 2222 --compact` (previously only the first form worked)
- **Non-JSON 200 responses** (e.g. proxy HTML errors) are now wrapped in `SahmkError` instead of raising a raw `ValueError`
- **`on_reconnect` docstring** corrected — it fires before a reconnect attempt (after backoff delay), not after a successful reconnection
- **Test fixtures** aligned with real API response shapes (market summary, company, financials, dividends, events)
- **`quotes([])` guard** — calling `quotes()` with an empty list now raises `ValueError` immediately instead of sending an invalid request
- **Redundant 429** removed from internal `_RETRIABLE_STATUS_CODES` (429 is handled by its own dedicated branch)

### Changed

- **PyPI classifier** updated from "3 - Alpha" to "4 - Beta"

## [0.6.0] — 2026-04-02

### Added
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "sahmk"
version = "0.6.0"
version = "0.6.1"
description = "Lightweight Python client for the SAHMK Developer API."
readme = "README.md"
requires-python = ">=3.9"
Expand All @@ -14,7 +14,7 @@ authors = [
]
keywords = ["stocks", "tadawul", "market-data", "websocket", "api"]
classifiers = [
"Development Status :: 3 - Alpha",
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
Expand Down
2 changes: 1 addition & 1 deletion sahmk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
Liquidity,
)

__version__ = "0.6.0"
__version__ = "0.6.1"
__all__ = [
"SahmkClient",
"SahmkError",
Expand Down
22 changes: 17 additions & 5 deletions sahmk/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@
from .client import SahmkClient, SahmkError


def _compact_arg(parser):
"""Add --compact flag to a parser."""
parser.add_argument(
"--compact",
action="store_true",
help="Print compact JSON output.",
)


def _build_parser():
parser = argparse.ArgumentParser(
prog="sahmk",
Expand All @@ -31,16 +40,12 @@ def _build_parser():
default=30,
help="Request timeout in seconds (default: 30).",
)
parser.add_argument(
"--compact",
action="store_true",
help="Print compact JSON output.",
)

subparsers = parser.add_subparsers(dest="command", required=True)

quote_parser = subparsers.add_parser("quote", help="Get a single stock quote.")
quote_parser.add_argument("symbol", help='Stock symbol (e.g., "2222").')
_compact_arg(quote_parser)

quotes_parser = subparsers.add_parser(
"quotes", help="Get quotes for multiple symbols."
Expand All @@ -49,6 +54,7 @@ def _build_parser():
"symbols",
help='Comma-separated symbols, e.g. "2222,1120,2010".',
)
_compact_arg(quotes_parser)

market_parser = subparsers.add_parser("market", help="Market overview endpoints.")
market_parser.add_argument(
Expand All @@ -61,6 +67,7 @@ def _build_parser():
type=int,
help="Optional limit for gainers/losers/volume/value.",
)
_compact_arg(market_parser)

historical_parser = subparsers.add_parser(
"historical", help="Get historical OHLCV data."
Expand All @@ -73,21 +80,25 @@ def _build_parser():
choices=["1d", "1w", "1m"],
help='Interval: "1d", "1w", or "1m".',
)
_compact_arg(historical_parser)

company_parser = subparsers.add_parser(
"company", help="Get company info (tiered by plan)."
)
company_parser.add_argument("symbol", help='Stock symbol (e.g., "2222").')
_compact_arg(company_parser)

financials_parser = subparsers.add_parser(
"financials", help="Get financial statements (Starter+ plan)."
)
financials_parser.add_argument("symbol", help='Stock symbol (e.g., "2222").')
_compact_arg(financials_parser)

dividends_parser = subparsers.add_parser(
"dividends", help="Get dividend history and yield (Starter+ plan)."
)
dividends_parser.add_argument("symbol", help='Stock symbol (e.g., "2222").')
_compact_arg(dividends_parser)

events_parser = subparsers.add_parser(
"events", help="Get AI-generated stock events (Pro+ plan)."
Expand All @@ -103,6 +114,7 @@ def _build_parser():
default=None,
help="Number of events to return.",
)
_compact_arg(events_parser)

stream_parser = subparsers.add_parser(
"stream", help="Stream real-time quotes via WebSocket (Pro+ plan)."
Expand Down
18 changes: 14 additions & 4 deletions sahmk/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

logger = logging.getLogger("sahmk")

_RETRIABLE_STATUS_CODES = frozenset({429, 500, 502, 503, 504})
_RETRIABLE_STATUS_CODES = frozenset({500, 502, 503, 504})


class SahmkError(Exception):
Expand Down Expand Up @@ -154,7 +154,14 @@ def _request(self, method, endpoint, params=None):
if response.status_code != 200:
raise self._build_api_error(response)

return response.json()
try:
return response.json()
except (ValueError, TypeError) as e:
raise SahmkError(
f"Unexpected non-JSON response: {e}",
status_code=response.status_code,
response=response,
)

raise last_exc # pragma: no cover

Expand Down Expand Up @@ -256,6 +263,8 @@ def quotes(self, symbols):
BatchQuotesResponse with .quotes list and .count
"""
from .models import BatchQuotesResponse
if not symbols:
raise ValueError("At least one symbol is required")
if len(symbols) > 50:
raise SahmkError("Maximum 50 symbols per batch request")
data = self._request(
Expand Down Expand Up @@ -478,8 +487,9 @@ async def stream(
on_error: Async callback — on_error(error_data)
on_disconnect: Async callback — on_disconnect(reason) called when
the connection drops. Receives a string reason.
on_reconnect: Async callback — on_reconnect(attempt) called after
a successful reconnection. Receives the attempt number.
on_reconnect: Async callback — on_reconnect(attempt) called before
a reconnect attempt (after the backoff delay).
Receives the attempt number.
ping_interval: Seconds between keep-alive pings (default: 30)
max_reconnect_attempts: Maximum reconnection attempts. 0 means
unlimited reconnection (default). Set to -1
Expand Down
Loading
Loading