stocksim-sdk 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,10 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
@@ -0,0 +1 @@
1
+ 3.14
@@ -0,0 +1,112 @@
1
+ Metadata-Version: 2.4
2
+ Name: stocksim-sdk
3
+ Version: 0.1.0
4
+ Summary: Official async Python SDK for StockSim Pro API
5
+ License: MIT
6
+ Requires-Python: >=3.10
7
+ Requires-Dist: httpx>=0.27
8
+ Provides-Extra: dev
9
+ Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
10
+ Requires-Dist: pytest>=8.0; extra == 'dev'
11
+ Requires-Dist: respx>=0.21; extra == 'dev'
12
+ Description-Content-Type: text/markdown
13
+
14
+ # stocksim-sdk
15
+
16
+ Official async Python SDK for [StockSim Pro](https://stocksim.pro).
17
+
18
+ - Python 3.13+
19
+ - Async-first (`asyncio` / `httpx`)
20
+ - Typed responses via `dataclasses`
21
+ - Zero heavy dependencies
22
+
23
+ ## Installation
24
+
25
+ ```bash
26
+ pip install stocksim-sdk
27
+ ```
28
+
29
+ ## Quick Start
30
+
31
+ ```python
32
+ import asyncio
33
+ from stocksim import StockSimClient
34
+
35
+ async def main():
36
+ async with StockSimClient(api_key="your-api-key") as client:
37
+ price = await client.get_price("AAPL")
38
+ print(price.ticker, price.price)
39
+
40
+ asyncio.run(main())
41
+ ```
42
+
43
+ ## Usage
44
+
45
+ ### Get price
46
+
47
+ ```python
48
+ async with StockSimClient(api_key="your-api-key") as client:
49
+ price = await client.get_price("TSLA")
50
+ # PriceResponse(ticker='TSLA', price=182.5, timestamp='2026-06-16T10:00:00Z')
51
+ print(f"{price.ticker}: ${price.price}")
52
+ ```
53
+
54
+ ### Get portfolio
55
+
56
+ ```python
57
+ async with StockSimClient(api_key="your-api-key") as client:
58
+ portfolio = await client.get_portfolio()
59
+ print(f"Balance: ${portfolio.balance:.2f}")
60
+ for pos in portfolio.positions:
61
+ print(f" {pos.ticker} qty={pos.quantity} avg=${pos.avg_price}")
62
+ ```
63
+
64
+ ### Place an order
65
+
66
+ ```python
67
+ async with StockSimClient(api_key="your-api-key") as client:
68
+ order = await client.place_order(ticker="AAPL", action="buy", quantity=5)
69
+ print(f"Order {order.order_id} — {order.status}")
70
+ ```
71
+
72
+ ## Error Handling
73
+
74
+ ```python
75
+ from stocksim import (
76
+ StockSimClient,
77
+ StockSimAuthError,
78
+ StockSimRateLimitError,
79
+ StockSimValidationError,
80
+ StockSimError,
81
+ )
82
+
83
+ async with StockSimClient(api_key="your-api-key") as client:
84
+ try:
85
+ price = await client.get_price("AAPL")
86
+ except StockSimAuthError:
87
+ print("Invalid or missing API key")
88
+ except StockSimRateLimitError as e:
89
+ retry = e.retry_after
90
+ print(f"Rate limit hit — retry after {retry}s")
91
+ except StockSimValidationError as e:
92
+ for err in e.errors:
93
+ print(f"Validation: {err['msg']} at {err['loc']}")
94
+ except StockSimError as e:
95
+ print(f"API error {e.status_code}: {e}")
96
+ ```
97
+
98
+ ## Manual session management
99
+
100
+ If you prefer not to use `async with`, call `close()` explicitly:
101
+
102
+ ```python
103
+ client = StockSimClient(api_key="your-api-key")
104
+ try:
105
+ price = await client.get_price("AAPL")
106
+ finally:
107
+ await client.close()
108
+ ```
109
+
110
+ ## License
111
+
112
+ MIT
@@ -0,0 +1,99 @@
1
+ # stocksim-sdk
2
+
3
+ Official async Python SDK for [StockSim Pro](https://stocksim.pro).
4
+
5
+ - Python 3.13+
6
+ - Async-first (`asyncio` / `httpx`)
7
+ - Typed responses via `dataclasses`
8
+ - Zero heavy dependencies
9
+
10
+ ## Installation
11
+
12
+ ```bash
13
+ pip install stocksim-sdk
14
+ ```
15
+
16
+ ## Quick Start
17
+
18
+ ```python
19
+ import asyncio
20
+ from stocksim import StockSimClient
21
+
22
+ async def main():
23
+ async with StockSimClient(api_key="your-api-key") as client:
24
+ price = await client.get_price("AAPL")
25
+ print(price.ticker, price.price)
26
+
27
+ asyncio.run(main())
28
+ ```
29
+
30
+ ## Usage
31
+
32
+ ### Get price
33
+
34
+ ```python
35
+ async with StockSimClient(api_key="your-api-key") as client:
36
+ price = await client.get_price("TSLA")
37
+ # PriceResponse(ticker='TSLA', price=182.5, timestamp='2026-06-16T10:00:00Z')
38
+ print(f"{price.ticker}: ${price.price}")
39
+ ```
40
+
41
+ ### Get portfolio
42
+
43
+ ```python
44
+ async with StockSimClient(api_key="your-api-key") as client:
45
+ portfolio = await client.get_portfolio()
46
+ print(f"Balance: ${portfolio.balance:.2f}")
47
+ for pos in portfolio.positions:
48
+ print(f" {pos.ticker} qty={pos.quantity} avg=${pos.avg_price}")
49
+ ```
50
+
51
+ ### Place an order
52
+
53
+ ```python
54
+ async with StockSimClient(api_key="your-api-key") as client:
55
+ order = await client.place_order(ticker="AAPL", action="buy", quantity=5)
56
+ print(f"Order {order.order_id} — {order.status}")
57
+ ```
58
+
59
+ ## Error Handling
60
+
61
+ ```python
62
+ from stocksim import (
63
+ StockSimClient,
64
+ StockSimAuthError,
65
+ StockSimRateLimitError,
66
+ StockSimValidationError,
67
+ StockSimError,
68
+ )
69
+
70
+ async with StockSimClient(api_key="your-api-key") as client:
71
+ try:
72
+ price = await client.get_price("AAPL")
73
+ except StockSimAuthError:
74
+ print("Invalid or missing API key")
75
+ except StockSimRateLimitError as e:
76
+ retry = e.retry_after
77
+ print(f"Rate limit hit — retry after {retry}s")
78
+ except StockSimValidationError as e:
79
+ for err in e.errors:
80
+ print(f"Validation: {err['msg']} at {err['loc']}")
81
+ except StockSimError as e:
82
+ print(f"API error {e.status_code}: {e}")
83
+ ```
84
+
85
+ ## Manual session management
86
+
87
+ If you prefer not to use `async with`, call `close()` explicitly:
88
+
89
+ ```python
90
+ client = StockSimClient(api_key="your-api-key")
91
+ try:
92
+ price = await client.get_price("AAPL")
93
+ finally:
94
+ await client.close()
95
+ ```
96
+
97
+ ## License
98
+
99
+ MIT
@@ -0,0 +1,26 @@
1
+ [project]
2
+ name = "stocksim-sdk"
3
+ version = "0.1.0"
4
+ description = "Official async Python SDK for StockSim Pro API"
5
+ readme = "README.md"
6
+ license = { text = "MIT" }
7
+ requires-python = ">=3.10"
8
+ dependencies = [
9
+ "httpx>=0.27",
10
+ ]
11
+
12
+ [project.optional-dependencies]
13
+ dev = [
14
+ "pytest>=8.0",
15
+ "pytest-asyncio>=0.24",
16
+ "respx>=0.21",
17
+ ]
18
+
19
+ [tool.uv.workspace]
20
+ members = [
21
+ "stocksim-sdk",
22
+ ]
23
+
24
+ [build-system]
25
+ requires = ["hatchling"]
26
+ build-backend = "hatchling.build"
@@ -0,0 +1,2 @@
1
+ def hello() -> str:
2
+ return "Hello from stocksim-sdk!"
@@ -0,0 +1,67 @@
1
+ from __future__ import annotations
2
+
3
+ from types import TracebackType
4
+ from typing import Any
5
+
6
+ import httpx
7
+
8
+ from stocksim.exceptions import _raise_for_status
9
+ from stocksim.models import OrderResponse, PortfolioResponse, PriceResponse
10
+
11
+ _DEFAULT_BASE_URL = "https://api.stocksim.pro"
12
+
13
+
14
+ class StockSimClient:
15
+ """Async HTTP client for the StockSim Pro API."""
16
+
17
+ def __init__(self, api_key: str, base_url: str = _DEFAULT_BASE_URL) -> None:
18
+ self._http = httpx.AsyncClient(
19
+ base_url=base_url,
20
+ headers={"X-API-Key": api_key},
21
+ timeout=30.0,
22
+ )
23
+
24
+ async def __aenter__(self) -> StockSimClient:
25
+ return self
26
+
27
+ async def __aexit__(
28
+ self,
29
+ exc_type: type[BaseException] | None,
30
+ exc_val: BaseException | None,
31
+ exc_tb: TracebackType | None,
32
+ ) -> None:
33
+ await self.close()
34
+
35
+ async def close(self) -> None:
36
+ """Close the underlying HTTP session."""
37
+ await self._http.aclose()
38
+
39
+ async def _request(self, method: str, endpoint: str, **kwargs: Any) -> Any:
40
+ response = await self._http.request(method, endpoint, **kwargs)
41
+ _raise_for_status(response)
42
+ return response.json()
43
+
44
+ async def get_price(self, ticker: str) -> PriceResponse:
45
+ """Fetch the current price for *ticker*."""
46
+ data = await self._request("GET", f"/prices/{ticker}")
47
+ return PriceResponse.from_dict(data)
48
+
49
+ async def get_portfolio(self) -> PortfolioResponse:
50
+ """Fetch the authenticated user's portfolio."""
51
+ data = await self._request("GET", "/portfolio")
52
+ return PortfolioResponse.from_dict(data)
53
+
54
+ async def place_order(self, ticker: str, action: str, quantity: int) -> OrderResponse:
55
+ """Place a buy/sell order.
56
+
57
+ Args:
58
+ ticker: Stock symbol, e.g. ``"AAPL"``.
59
+ action: ``"buy"`` or ``"sell"``.
60
+ quantity: Number of shares.
61
+ """
62
+ data = await self._request(
63
+ "POST",
64
+ "/orders",
65
+ json={"ticker": ticker, "action": action, "quantity": quantity},
66
+ )
67
+ return OrderResponse.from_dict(data)
@@ -0,0 +1,75 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ import httpx
6
+
7
+
8
+ class StockSimError(Exception):
9
+ """Base exception for all StockSim SDK errors."""
10
+
11
+ def __init__(self, message: str, *, status_code: int | None = None, detail: Any = None) -> None:
12
+ super().__init__(message)
13
+ self.status_code = status_code
14
+ self.detail = detail
15
+
16
+ def __repr__(self) -> str:
17
+ return f"{type(self).__name__}({self.args[0]!r}, status_code={self.status_code})"
18
+
19
+
20
+ class StockSimAuthError(StockSimError):
21
+ """Raised on 401 Unauthorized or 403 Forbidden responses."""
22
+
23
+
24
+ class StockSimRateLimitError(StockSimError):
25
+ """Raised on 429 Too Many Requests responses."""
26
+
27
+ def __init__(self, message: str, *, status_code: int = 429, detail: Any = None, retry_after: int | None = None) -> None:
28
+ super().__init__(message, status_code=status_code, detail=detail)
29
+ self.retry_after = retry_after
30
+
31
+
32
+ class StockSimValidationError(StockSimError):
33
+ """Raised on 422 Unprocessable Entity responses (FastAPI validation errors)."""
34
+
35
+ def __init__(self, message: str, *, status_code: int = 422, detail: Any = None) -> None:
36
+ super().__init__(message, status_code=status_code, detail=detail)
37
+ self.errors: list[dict[str, Any]] = detail if isinstance(detail, list) else []
38
+
39
+
40
+ def _raise_for_status(response: httpx.Response) -> None:
41
+ """Parse an error response from the StockSim API and raise the appropriate exception."""
42
+ if response.is_success:
43
+ return
44
+
45
+ status_code = response.status_code
46
+ detail: Any = None
47
+ message = f"HTTP {status_code}"
48
+
49
+ try:
50
+ body = response.json()
51
+ detail = body.get("detail", body)
52
+ if isinstance(detail, str):
53
+ message = detail
54
+ elif isinstance(detail, list) and detail:
55
+ first = detail[0]
56
+ if isinstance(first, dict):
57
+ message = first.get("msg", message)
58
+ except Exception:
59
+ message = response.text or message
60
+
61
+ if status_code in (401, 403):
62
+ raise StockSimAuthError(message, status_code=status_code, detail=detail)
63
+ if status_code == 422:
64
+ raise StockSimValidationError(message, status_code=status_code, detail=detail)
65
+ if status_code == 429:
66
+ retry_after: int | None = None
67
+ raw = response.headers.get("Retry-After")
68
+ if raw is not None:
69
+ try:
70
+ retry_after = int(raw)
71
+ except ValueError:
72
+ pass
73
+ raise StockSimRateLimitError(message, status_code=status_code, detail=detail, retry_after=retry_after)
74
+
75
+ raise StockSimError(message, status_code=status_code, detail=detail)
@@ -0,0 +1,66 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any, Self
5
+
6
+
7
+ @dataclass(frozen=True)
8
+ class PriceResponse:
9
+ ticker: str
10
+ price: float
11
+ timestamp: str
12
+
13
+ @classmethod
14
+ def from_dict(cls, data: dict[str, Any]) -> Self:
15
+ return cls(
16
+ ticker=data["ticker"],
17
+ price=float(data["price"]),
18
+ timestamp=data["timestamp"],
19
+ )
20
+
21
+
22
+ @dataclass(frozen=True)
23
+ class PortfolioPosition:
24
+ ticker: str
25
+ quantity: int
26
+ avg_price: float
27
+
28
+ @classmethod
29
+ def from_dict(cls, data: dict[str, Any]) -> Self:
30
+ return cls(
31
+ ticker=data["ticker"],
32
+ quantity=int(data["quantity"]),
33
+ avg_price=float(data["avg_price"]),
34
+ )
35
+
36
+
37
+ @dataclass(frozen=True)
38
+ class PortfolioResponse:
39
+ balance: float
40
+ positions: list[PortfolioPosition]
41
+
42
+ @classmethod
43
+ def from_dict(cls, data: dict[str, Any]) -> Self:
44
+ return cls(
45
+ balance=float(data["balance"]),
46
+ positions=[PortfolioPosition.from_dict(p) for p in data.get("positions", [])],
47
+ )
48
+
49
+
50
+ @dataclass(frozen=True)
51
+ class OrderResponse:
52
+ order_id: str
53
+ ticker: str
54
+ action: str
55
+ quantity: int
56
+ status: str
57
+
58
+ @classmethod
59
+ def from_dict(cls, data: dict[str, Any]) -> Self:
60
+ return cls(
61
+ order_id=data["order_id"],
62
+ ticker=data["ticker"],
63
+ action=data["action"],
64
+ quantity=int(data["quantity"]),
65
+ status=data["status"],
66
+ )
File without changes
File without changes
@@ -0,0 +1,141 @@
1
+ from __future__ import annotations
2
+
3
+ import pytest
4
+ import respx
5
+ from httpx import Response
6
+
7
+ from stocksim import (
8
+ StockSimAuthError,
9
+ StockSimClient,
10
+ StockSimRateLimitError,
11
+ StockSimValidationError,
12
+ )
13
+ from stocksim.models import OrderResponse, PortfolioResponse, PriceResponse
14
+
15
+ BASE = "https://api.stocksim.pro"
16
+
17
+
18
+ @pytest.fixture
19
+ def client() -> StockSimClient:
20
+ return StockSimClient(api_key="test-key", base_url=BASE)
21
+
22
+
23
+ @respx.mock
24
+ @pytest.mark.asyncio
25
+ async def test_get_price_success(client: StockSimClient) -> None:
26
+ respx.get(f"{BASE}/prices/AAPL").mock(
27
+ return_value=Response(200, json={"ticker": "AAPL", "price": 182.5, "timestamp": "2026-06-16T10:00:00Z"})
28
+ )
29
+
30
+ result = await client.get_price("AAPL")
31
+
32
+ assert isinstance(result, PriceResponse)
33
+ assert result.ticker == "AAPL"
34
+ assert result.price == 182.5
35
+ assert result.timestamp == "2026-06-16T10:00:00Z"
36
+
37
+
38
+ @respx.mock
39
+ @pytest.mark.asyncio
40
+ async def test_get_portfolio_success(client: StockSimClient) -> None:
41
+ respx.get(f"{BASE}/portfolio").mock(
42
+ return_value=Response(
43
+ 200,
44
+ json={
45
+ "balance": 10000.0,
46
+ "positions": [{"ticker": "AAPL", "quantity": 5, "avg_price": 175.0}],
47
+ },
48
+ )
49
+ )
50
+
51
+ result = await client.get_portfolio()
52
+
53
+ assert isinstance(result, PortfolioResponse)
54
+ assert result.balance == 10000.0
55
+ assert len(result.positions) == 1
56
+ assert result.positions[0].ticker == "AAPL"
57
+ assert result.positions[0].quantity == 5
58
+
59
+
60
+ @respx.mock
61
+ @pytest.mark.asyncio
62
+ async def test_place_order_success(client: StockSimClient) -> None:
63
+ respx.post(f"{BASE}/orders").mock(
64
+ return_value=Response(
65
+ 201,
66
+ json={"order_id": "ord-123", "ticker": "TSLA", "action": "buy", "quantity": 10, "status": "filled"},
67
+ )
68
+ )
69
+
70
+ result = await client.place_order("TSLA", "buy", 10)
71
+
72
+ assert isinstance(result, OrderResponse)
73
+ assert result.order_id == "ord-123"
74
+ assert result.action == "buy"
75
+ assert result.status == "filled"
76
+
77
+
78
+ @respx.mock
79
+ @pytest.mark.asyncio
80
+ async def test_auth_error_raises_StockSimAuthError(client: StockSimClient) -> None:
81
+ respx.get(f"{BASE}/portfolio").mock(
82
+ return_value=Response(401, json={"detail": "Invalid API key"})
83
+ )
84
+
85
+ with pytest.raises(StockSimAuthError) as exc_info:
86
+ await client.get_portfolio()
87
+
88
+ assert exc_info.value.status_code == 401
89
+ assert "Invalid API key" in str(exc_info.value)
90
+
91
+
92
+ @respx.mock
93
+ @pytest.mark.asyncio
94
+ async def test_forbidden_raises_StockSimAuthError(client: StockSimClient) -> None:
95
+ respx.get(f"{BASE}/portfolio").mock(
96
+ return_value=Response(403, json={"detail": "Forbidden"})
97
+ )
98
+
99
+ with pytest.raises(StockSimAuthError) as exc_info:
100
+ await client.get_portfolio()
101
+
102
+ assert exc_info.value.status_code == 403
103
+
104
+
105
+ @respx.mock
106
+ @pytest.mark.asyncio
107
+ async def test_rate_limit_raises_StockSimRateLimitError(client: StockSimClient) -> None:
108
+ respx.get(f"{BASE}/prices/AAPL").mock(
109
+ return_value=Response(
110
+ 429,
111
+ json={"detail": "Rate limit exceeded"},
112
+ headers={"Retry-After": "30"},
113
+ )
114
+ )
115
+
116
+ with pytest.raises(StockSimRateLimitError) as exc_info:
117
+ await client.get_price("AAPL")
118
+
119
+ assert exc_info.value.status_code == 429
120
+ assert exc_info.value.retry_after == 30
121
+
122
+
123
+ @respx.mock
124
+ @pytest.mark.asyncio
125
+ async def test_validation_error_raises_StockSimValidationError(client: StockSimClient) -> None:
126
+ respx.post(f"{BASE}/orders").mock(
127
+ return_value=Response(
128
+ 422,
129
+ json={
130
+ "detail": [
131
+ {"loc": ["body", "quantity"], "msg": "value is not a valid integer", "type": "type_error.integer"}
132
+ ]
133
+ },
134
+ )
135
+ )
136
+
137
+ with pytest.raises(StockSimValidationError) as exc_info:
138
+ await client.place_order("AAPL", "buy", -1)
139
+
140
+ assert exc_info.value.status_code == 422
141
+ assert len(exc_info.value.errors) == 1