@@ -101,39 +101,115 @@ result = await sharesight.get_api_request(["v3", "portfolios", None], access_tok
101101the client to refresh a caller-stored token. A refresh response that omits a
102102replacement refresh token retains the token that just succeeded.
103103
104- ## Convenience methods
104+ ## Typed convenience methods
105+
106+ The client prefers public v3 endpoints, as Sharesight recommends, and uses v2
107+ where v3 has no equivalent public aggregate. Response annotations come from
108+ the partial ` TypedDict ` models exported by ` SharesightAPI ` ; fields remain
109+ optional because Sharesight omits data for empty, sold, delisted and
110+ plan-limited positions.
105111
106112``` python
107- portfolios = await sharesight.list_portfolios()
108- portfolio = await sharesight.get_portfolio(portfolio_id)
109- performance = await sharesight.get_portfolio_performance(
113+ portfolios = await sharesight.list_portfolios_v3()
114+ portfolio = await sharesight.get_portfolio_v3(portfolio_id)
115+ # Stable V2 helpers (the unsuffixed list/detail/performance methods preserve
116+ # their pre-1.5 routes and response shapes):
117+ portfolios_v2 = await sharesight.list_portfolios_v2()
118+ portfolio_v2 = await sharesight.get_portfolio_v2(portfolio_id)
119+ performance = await sharesight.get_portfolio_performance_v3(
110120 portfolio_id,
111121 start_date = " 2026-01-01" ,
112122 end_date = " 2026-08-27" ,
113123)
124+ performance_v2 = await sharesight.get_portfolio_performance_v2(portfolio_id)
114125holdings = await sharesight.list_holdings(portfolio_id)
126+ all_holdings = await sharesight.list_all_holdings()
115127holding = await sharesight.get_holding(holding_id)
116128trades = await sharesight.list_trades(portfolio_id)
117- trade = await sharesight.create_trade (portfolio_id, trade_data )
129+ payouts = await sharesight.list_portfolio_payouts (portfolio_id)
118130cash_accounts = await sharesight.list_cash_accounts()
119131cash_account = await sharesight.get_cash_account(cash_account_id)
132+ cash_transactions = await sharesight.list_cash_account_transactions(cash_account_id)
133+ benchmark = await sharesight.get_portfolio_benchmark(portfolio_id)
134+ value_history = await sharesight.get_portfolio_value_data(portfolio_id)
135+ capital_gains = await sharesight.get_capital_gains(portfolio_id)
136+ unrealised_cgt = await sharesight.get_unrealised_cgt(portfolio_id)
120137groups = await sharesight.list_groups()
138+ currencies = await sharesight.list_currencies()
139+ countries = await sharesight.list_countries(supported = True )
140+ custom_investments = await sharesight.list_custom_investments(portfolio_id = portfolio_id)
141+ custom_investment = await sharesight.get_custom_investment(custom_investment_id)
142+ custom_prices = await sharesight.list_custom_investment_prices(custom_investment_id)
143+ custom_adjustments = await sharesight.list_custom_investment_adjustments(custom_investment_id)
144+ coupon_rates = await sharesight.list_custom_investment_coupon_rates(custom_investment_id)
121145```
122146
147+ | Data | Preferred endpoint |
148+ | ---| ---|
149+ | Portfolio list/detail | V3 ` portfolios ` , ` portfolios/{id} ` |
150+ | Performance/holdings | Public V3 portfolio routes |
151+ | Trades and holding payouts | Public V2 routes (the V3 equivalents are internal-scoped) |
152+ | Valuation/diversity/tax/payout aggregates | V2 portfolio routes |
153+ | Cash accounts/transactions | V2 cash-account routes |
154+ | Benchmark | V3 internal-tagged route; entitlement-dependent |
155+ | Value series | V3 mobile-tagged route; entitlement-dependent |
156+ | Performance index | Public V3 portfolio route |
157+ | User instruments/account/groups/currencies | Public V2 routes |
158+ | Countries/custom-investment reads | Public V3 routes |
159+
160+ ` create_trade() ` is intentionally separated from the read-only group because
161+ it mutates financial records. Test it with mocks or a Sharesight developer
162+ sandbox before any authorised live use.
163+
164+ ## Pagination and monetary precision
165+
166+ The aggregate endpoints above return complete arrays. For the documented V3
167+ custom-investment routes that paginate, the generic collector follows the
168+ opaque cursor returned in ` pagination.page ` :
169+
170+ ``` python
171+ all_prices = await sharesight.get_all_pages(
172+ [" v3" , f " custom_investment/ { custom_investment_id} /prices.json " , None ],
173+ item_key = " prices" ,
174+ per_page = 100 ,
175+ )
176+ ```
177+
178+ Pagination is bounded by ` max_pages ` and rejects malformed or repeated cursors
179+ with ` SharesightResponseError ` . You can also pass the returned
180+ ` pagination.page ` string through each dedicated helper's ` page= ` argument.
181+ The collector never guesses another page from array length, so using it with a
182+ non-paginated aggregate endpoint cannot duplicate a large response.
183+
184+ To preserve fractional JSON numbers as decimal values rather than binary
185+ floats:
186+
187+ ``` python
188+ sharesight = SharesightAPI(... , preserve_decimal = True )
189+ ```
190+
191+ Date-only and datetime values remain the exact strings supplied by Sharesight
192+ so applications can apply their own timezone policy without the client
193+ inventing one. Most are ISO-8601; a few legacy V2 portfolio fields use display
194+ formats such as ` 01 Jan 2009 ` .
195+
123196## Raw requests
124197
125198An endpoint is ` [version, path, query_parameters] ` :
126199
127200``` python
128201portfolios = await sharesight.get_api_request([" v3" , " portfolios" , None ], access_token)
129202
130- trade = await sharesight.post_api_request(
131- [" v2" , f " portfolios/ { portfolio_id} /trades " , {" dry_run" : " true" }],
132- {" trade" : trade_data},
203+ value_history = await sharesight.get_api_request(
204+ [" v3" , f " portfolios/ { portfolio_id} /portfolio_value_data.json " , None ],
133205 access_token,
134206)
135207```
136208
209+ The raw ` POST ` , ` PUT ` , ` PATCH ` , and ` DELETE ` helpers can mutate Sharesight
210+ records. Keep those calls outside polling code and validate them with mocks or
211+ an authorised developer sandbox.
212+
137213The official endpoint references are available for
138214[ v2] ( https://portfolio.sharesight.com/api/2/doc/index.html ) and
139215[ v3] ( https://portfolio.sharesight.com/api/3/doc/index.html ) .
@@ -164,15 +240,20 @@ from SharesightAPI import (
164240 SharesightAuthError,
165241 SharesightError,
166242 SharesightRateLimitError,
243+ SharesightResponseError,
167244)
168245```
169246
170247- ` SharesightError ` is the base exception.
171- - ` SharesightAuthError ` retains authentication status, body, and headers.
248+ - ` SharesightAuthError ` retains authentication status and headers. Normal API
249+ errors retain their structured body; token-endpoint bodies are reduced to a
250+ safe OAuth error code because providers may echo credentials in free text.
172251- ` SharesightAPIError ` exposes ` status_code ` , ` message ` , ` response_data ` , and
173252 ` response_headers ` .
174253- ` SharesightRateLimitError ` represents HTTP 429 and Sharesight's rate-limit
175254 HTTP 403, and may expose ` retry_after ` .
255+ - ` SharesightResponseError ` represents a successful response with a malformed
256+ or non-advancing shape.
176257
177258By default, failures return a body for backward compatibility. JSON error
178259bodies gain ` status_code ` when the server omitted it. Opt into exceptions with
@@ -205,11 +286,15 @@ sharesight = SharesightAPI(
205286 api_url_base,
206287 max_retries = 3 ,
207288 retry_backoff = 1.0 ,
289+ retry_jitter = 0.25 ,
290+ max_retry_delay = 300 ,
291+ request_timeout = 30 ,
208292)
209293```
210294
211- Backoff doubles after each failure. Numeric ` Retry-After ` values are respected
212- and capped at five minutes. Set ` max_retries=0 ` when a host application owns
295+ Backoff doubles after each failure, adds bounded positive jitter, and never
296+ exceeds ` max_retry_delay ` . Numeric ` Retry-After ` values are respected and
297+ capped by the same limit. Set ` max_retries=0 ` when a host application owns
213298scheduling and rate-limit backoff; this surfaces the rejection immediately
214299instead of sleeping inside the request.
215300
@@ -227,6 +312,8 @@ python -m pip install -r requirements_test.txt
227312python -m pip install -e .
228313python -m pytest
229314python -m ruff check .
315+ python -m ruff format --check .
316+ python -m mypy SharesightAPI tests/typecheck_models.py
230317python -m build
231318python -m twine check dist/*
232319python scripts/check_dist.py dist
0 commit comments