Skip to content

Commit b0aeec3

Browse files
committed
Prepare SharesightAPI 1.5.0
1 parent 6fc691e commit b0aeec3

18 files changed

Lines changed: 2704 additions & 156 deletions

.github/workflows/publish.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ jobs:
3737
run: |
3838
python -m ruff check .
3939
python -m ruff format --check .
40+
python -m mypy SharesightAPI tests/typecheck_models.py
4041
- name: Build distributions
4142
run: python -m build
4243
- name: Check distribution metadata and contents

.github/workflows/test.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ jobs:
3939
- run: python -m pip install -r requirements_test.txt
4040
- run: python -m ruff check .
4141
- run: python -m ruff format --check .
42+
- run: python -m mypy SharesightAPI tests/typecheck_models.py
4243
- run: python -m build
4344
- run: python -m twine check dist/*
4445
- run: python scripts/check_dist.py dist

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ output.json
77

88
/.pytest_cache/
99
/.ruff_cache/
10+
/.mypy_cache/
11+
/.coverage
12+
/coverage.xml
13+
/htmlcov/
1014
/.venv/
1115

1216
/build

CHANGELOG.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,50 @@
33
All notable changes to this project are documented here. Versions follow
44
[Semantic Versioning](https://semver.org/).
55

6+
## [1.5.0] - 2026-08-31
7+
8+
### Added
9+
10+
- Added partial `TypedDict` response models for portfolios, holdings,
11+
performance, trades, payouts, cash accounts, groups, currencies, countries,
12+
custom investments, prices, adjustments, coupon rates and value series.
13+
Missing and nullable API fields remain valid.
14+
- Added typed read-only helpers for the public v2/v3 portfolio, performance,
15+
valuation, diversity, tax, holding, trade, payout, cash, instrument, user,
16+
group, currency, country and custom-investment endpoints, plus clearly
17+
labelled entitlement-dependent benchmark and value-series reads.
18+
- Added bounded opaque-cursor pagination for Sharesight's documented paginated
19+
routes, with repeated-cursor protection and no unsafe page-count guessing.
20+
- Added opt-in `Decimal` JSON decoding, per-request timeouts, bounded retry
21+
jitter and a typed malformed-response exception.
22+
23+
### Changed
24+
25+
- Added explicit preferred-V3 helpers while preserving the legacy V2 response
26+
contracts of `list_portfolios()`, `get_portfolio()` and
27+
`get_portfolio_performance()`; corrected official `.json` route spellings
28+
where required.
29+
- Portfolio/holding trade lists and holding payouts now deliberately use the
30+
public V2 routes; Sharesight marks their V3 counterparts as internal-only.
31+
- Added explicit public V2 portfolio list, detail and performance fallbacks;
32+
modelled the V2 portfolio/cash-account and V3 custom-investment detail routes
33+
as the bare objects they actually return.
34+
- Custom-investment child reads accept and preserve Sharesight's opaque page
35+
cursors rather than assuming numeric pages.
36+
- Corrected the bundled example's client construction and V2 route spelling;
37+
it no longer prints OAuth token data or writes portfolio payloads unless the
38+
user explicitly opts in.
39+
- Corrected `create_trade()` to post to `v2/trades.json`, wrap the trade body,
40+
and inject the portfolio id. It remains explicitly isolated as a
41+
write-capable method and is covered only with mocks.
42+
- Token-file existence, replacement, permissions and deletion are now
43+
asynchronous; unique atomic temporary files and legacy/new owner-only
44+
permissions prevent predictable-symlink and brief-readable-file windows.
45+
- OAuth log redaction now recursively removes nested credentials and writes
46+
only allowlisted metadata; token exceptions never retain raw error bodies.
47+
48+
[1.5.0]: https://github.com/Poshy163/Sharesight-API/compare/v1.4.0...v1.5.0
49+
650
## [1.4.0] - 2026-08-27
751

852
### Added

MANIFEST.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
include CHANGELOG.md
2+
include example.py
23
include LICENSE
34
include README.md
45
include RELEASING.md

README.md

Lines changed: 98 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -101,39 +101,115 @@ result = await sharesight.get_api_request(["v3", "portfolios", None], access_tok
101101
the client to refresh a caller-stored token. A refresh response that omits a
102102
replacement 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)
114125
holdings = await sharesight.list_holdings(portfolio_id)
126+
all_holdings = await sharesight.list_all_holdings()
115127
holding = await sharesight.get_holding(holding_id)
116128
trades = 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)
118130
cash_accounts = await sharesight.list_cash_accounts()
119131
cash_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)
120137
groups = 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

125198
An endpoint is `[version, path, query_parameters]`:
126199

127200
```python
128201
portfolios = 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+
137213
The 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

177258
By default, failures return a body for backward compatibility. JSON error
178259
bodies 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
213298
scheduling and rate-limit backoff; this surfaces the rejection immediately
214299
instead of sleeping inside the request.
215300

@@ -227,6 +312,8 @@ python -m pip install -r requirements_test.txt
227312
python -m pip install -e .
228313
python -m pytest
229314
python -m ruff check .
315+
python -m ruff format --check .
316+
python -m mypy SharesightAPI tests/typecheck_models.py
230317
python -m build
231318
python -m twine check dist/*
232319
python scripts/check_dist.py dist

RELEASING.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ for the project-side setup.
3333
python -m pip install -e .
3434
python -m pytest
3535
python -m ruff check .
36+
python -m mypy SharesightAPI tests/typecheck_models.py
3637
python -m build
3738
python -m twine check dist/*
3839
python scripts/check_dist.py dist
@@ -41,16 +42,17 @@ for the project-side setup.
4142
4. Commit and push the reviewed release changes. Confirm the `Test` workflow
4243
succeeds on that exact commit.
4344
5. Create a GitHub release whose tag is exactly `v<package version>`, for
44-
example `v1.4.0`, targeting the validated commit.
45+
example `v1.5.0`, targeting the validated commit.
4546
6. Publish the GitHub release. The `Publish Python package` workflow checks
4647
that the tag and package versions match, rebuilds the distributions, and
4748
publishes them through the `pypi` environment.
4849
7. Verify the files and metadata on PyPI, then install into a clean environment:
4950

5051
```bash
52+
RELEASE_VERSION="$(python -c 'from SharesightAPI import __version__; print(__version__)')"
5153
python -m venv release-smoke
52-
release-smoke/bin/python -m pip install SharesightAPI==1.4.0
53-
release-smoke/bin/python -c "import SharesightAPI; print(SharesightAPI.__version__)"
54+
release-smoke/bin/python -m pip install "SharesightAPI==$RELEASE_VERSION"
55+
release-smoke/bin/python -I -c "import SharesightAPI; print(SharesightAPI.__version__)"
5456
```
5557

5658
On Windows, use `release-smoke\Scripts\python.exe`.

0 commit comments

Comments
 (0)