traderepublic-sync 0.3.1__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.
Files changed (27) hide show
  1. traderepublic_sync-0.3.1/LICENSE +21 -0
  2. traderepublic_sync-0.3.1/PKG-INFO +373 -0
  3. traderepublic_sync-0.3.1/README.md +339 -0
  4. traderepublic_sync-0.3.1/pyproject.toml +56 -0
  5. traderepublic_sync-0.3.1/setup.cfg +4 -0
  6. traderepublic_sync-0.3.1/src/traderepublic_sync/__init__.py +60 -0
  7. traderepublic_sync-0.3.1/src/traderepublic_sync/_classify.py +101 -0
  8. traderepublic_sync-0.3.1/src/traderepublic_sync/client.py +940 -0
  9. traderepublic_sync-0.3.1/src/traderepublic_sync/constants.py +27 -0
  10. traderepublic_sync-0.3.1/src/traderepublic_sync/dual_legged/__init__.py +23 -0
  11. traderepublic_sync-0.3.1/src/traderepublic_sync/dual_legged/mapping.py +300 -0
  12. traderepublic_sync-0.3.1/src/traderepublic_sync/exceptions.py +51 -0
  13. traderepublic_sync-0.3.1/src/traderepublic_sync/parsing.py +235 -0
  14. traderepublic_sync-0.3.1/src/traderepublic_sync/py.typed +0 -0
  15. traderepublic_sync-0.3.1/src/traderepublic_sync/session.py +385 -0
  16. traderepublic_sync-0.3.1/src/traderepublic_sync/state.py +56 -0
  17. traderepublic_sync-0.3.1/src/traderepublic_sync/waf.py +142 -0
  18. traderepublic_sync-0.3.1/src/traderepublic_sync.egg-info/PKG-INFO +373 -0
  19. traderepublic_sync-0.3.1/src/traderepublic_sync.egg-info/SOURCES.txt +25 -0
  20. traderepublic_sync-0.3.1/src/traderepublic_sync.egg-info/dependency_links.txt +1 -0
  21. traderepublic_sync-0.3.1/src/traderepublic_sync.egg-info/requires.txt +13 -0
  22. traderepublic_sync-0.3.1/src/traderepublic_sync.egg-info/top_level.txt +1 -0
  23. traderepublic_sync-0.3.1/tests/test_classify.py +267 -0
  24. traderepublic_sync-0.3.1/tests/test_dual_legged.py +270 -0
  25. traderepublic_sync-0.3.1/tests/test_fetch_filters.py +327 -0
  26. traderepublic_sync-0.3.1/tests/test_parsing.py +53 -0
  27. traderepublic_sync-0.3.1/tests/test_parsing_fixtures.py +102 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Helios de Creisquer
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,373 @@
1
+ Metadata-Version: 2.4
2
+ Name: traderepublic-sync
3
+ Version: 0.3.1
4
+ Summary: Unofficial Trade Republic sync library: WAF acquisition, login + 2FA, WebSocket data fetching, transaction parsing.
5
+ Author-email: Helios de Creisquer <hdecreis@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/hdecreis/libtrsync
8
+ Project-URL: Source, https://github.com/hdecreis/libtrsync
9
+ Project-URL: Issues, https://github.com/hdecreis/libtrsync/issues
10
+ Project-URL: Changelog, https://github.com/hdecreis/libtrsync/blob/main/CHANGELOG.md
11
+ Keywords: trade-republic,traderepublic,finance,broker,sync,scraping
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Office/Business :: Financial
20
+ Requires-Python: >=3.11
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: websockets<17,>=15
24
+ Requires-Dist: requests<3,>=2.31
25
+ Provides-Extra: playwright
26
+ Requires-Dist: playwright>=1.52; extra == "playwright"
27
+ Provides-Extra: selenium
28
+ Requires-Dist: selenium>=4; extra == "selenium"
29
+ Provides-Extra: dev
30
+ Requires-Dist: pytest>=8; extra == "dev"
31
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
32
+ Requires-Dist: ruff>=0.5; extra == "dev"
33
+ Dynamic: license-file
34
+
35
+ # traderepublic-sync
36
+
37
+ [![CI](https://github.com/hdecreis/libtrsync/actions/workflows/ci.yml/badge.svg)](https://github.com/hdecreis/libtrsync/actions/workflows/ci.yml)
38
+
39
+ Unofficial Python client for **Trade Republic**. Handles AWS WAF token
40
+ acquisition, phone+PIN login with 2FA, WebSocket data fetching, and parsing
41
+ of the timeline-detail responses into structured Python dicts.
42
+
43
+ > ⚠️ **Unofficial.** Trade Republic does not publish an API. This library
44
+ > reverse-engineers the web app's WebSocket protocol; it can break at any
45
+ > time and is not endorsed by Trade Republic. Use at your own risk.
46
+
47
+ ## Install
48
+
49
+ ```bash
50
+ # Base install (websockets + requests only)
51
+ pip install -e .
52
+
53
+ # With Playwright for WAF token acquisition (recommended)
54
+ pip install -e .[playwright]
55
+ playwright install chromium
56
+
57
+ # Or with Selenium
58
+ pip install -e .[selenium]
59
+ ```
60
+
61
+ Python ≥ 3.11.
62
+
63
+ ## Quickstart
64
+
65
+ ```python
66
+ import asyncio
67
+ from traderepublic_sync import TRClient
68
+
69
+ client = TRClient(locale="fr")
70
+
71
+ # 1. WAF token (uses headless browser)
72
+ client.acquire_waf_token("playwright")
73
+
74
+ # 2. Login - Trade Republic will push a 2FA prompt to your phone
75
+ login = client.login(phone_number="+33612121212", pin="1234")
76
+ print(f"2FA code requested. You have {login['countdown']}s.")
77
+
78
+ # Optional: ask for SMS instead of the in-app push
79
+ # client.request_sms(login["process_id"])
80
+
81
+ # 3. Verify 2FA (read the code from wherever the user enters it)
82
+ code = input("2FA code: ")
83
+ session_token = client.verify_2fa(login["process_id"], code)
84
+
85
+ # 4. Fetch data
86
+ result = asyncio.run(client.fetch_transactions(session_token))
87
+ print(f"{len(result['transactions'])} transactions, "
88
+ f"{len(result['raw_items'])} raw items")
89
+
90
+ balance = asyncio.run(client.fetch_cash_balance(session_token))
91
+ print("Cash:", balance)
92
+ ```
93
+
94
+ ## Persisting session state
95
+
96
+ `ConnectionState` is a plain dataclass - pickle it, JSON-encode it, or store
97
+ it in your own DB. The WAF token + session token can be reused across
98
+ processes until they expire (typically a few hours).
99
+
100
+ ```python
101
+ from dataclasses import asdict
102
+ import json
103
+ from traderepublic_sync import ConnectionState, TRClient
104
+
105
+ # Save after a successful login
106
+ state = ConnectionState(
107
+ phone_number="+33612121212",
108
+ pin="1234",
109
+ waf_token=client.waf_token,
110
+ device_info=client.device_info,
111
+ session_token=session_token,
112
+ auth_status="authenticated",
113
+ )
114
+ with open("tr_state.json", "w") as f:
115
+ json.dump(asdict(state), f)
116
+
117
+ # Restore later
118
+ with open("tr_state.json") as f:
119
+ state = ConnectionState(**json.load(f))
120
+
121
+ client = TRClient(waf_token=state.waf_token, device_info=state.device_info)
122
+ asyncio.run(client.fetch_transactions(state.session_token))
123
+ ```
124
+
125
+ ## Dual-legged transactions (optional)
126
+
127
+ The mapping layer shapes TR events into a double-entry transaction schema
128
+ (PURCHASE / SELL / DIVIDEND / TRANSFER / …) with explicit credit / debit /
129
+ fee / tax legs. It lives in a separate submodule so generic users can ignore it:
130
+
131
+ ```python
132
+ from traderepublic_sync.dual_legged import (
133
+ build_dual_legged_transaction,
134
+ deduplicate_pea,
135
+ EVENT_TYPE_MAP,
136
+ )
137
+
138
+ # Given a raw TR item + its parsed detail (use parse_detail_sections from
139
+ # the main package), produce a dual-legged transaction dict:
140
+ tx = build_dual_legged_transaction(raw_item, parsed_detail)
141
+ ```
142
+
143
+ `fetch_transactions()` already applies this mapping and returns both forms
144
+ under `"transactions"` (dual-legged) and `"raw_items"` (raw TR items with the
145
+ parsed detail attached as `_detail` / `_detail_raw`).
146
+
147
+ ## Live subscriptions (TRSession)
148
+
149
+ `TRClient` exposes one-shot helpers (`fetch_transactions`, `fetch_cash_balance`,
150
+ `fetch_ticker`, …) that open a WebSocket, send a single request, and close.
151
+ For **streaming** use cases — live ticker, live portfolio updates, instrument
152
+ search — open a long-lived session via `client.open_session()`.
153
+
154
+ ```python
155
+ import asyncio
156
+ from traderepublic_sync import TRClient
157
+
158
+ client = TRClient(waf_token=..., device_info=...)
159
+ # ...login + verify_2fa as in the quickstart...
160
+
161
+ async def watch_apple():
162
+ async with client.open_session(session_token) as session:
163
+ def on_tick(data):
164
+ last = (data.get("last") or {}).get("price")
165
+ print(f"AAPL = {last}")
166
+
167
+ sub_id = await session.subscribe_ticker("US0378331005", on_tick)
168
+ await asyncio.sleep(60) # stream for a minute
169
+ await session.unsubscribe(sub_id) # optional — __aexit__ also cleans up
170
+
171
+ asyncio.run(watch_apple())
172
+ ```
173
+
174
+ ### Convenience subscriptions
175
+
176
+ | Method | What it streams |
177
+ |---|---|
178
+ | `subscribe_ticker(isin, cb)` | Live price (`last`, `bid`, `ask`, `open`, `pre`) — resolves the home exchange for you |
179
+ | `subscribe_portfolio(sec_acc_no, cb)` | Live positions list (quantity + cost basis) |
180
+ | `subscribe_cash(cash_acc_no, cb)` | Available cash balance |
181
+ | `subscribe_transactions(cash_acc_no, cb)` | Timeline transactions as they appear |
182
+
183
+ All callbacks may be plain functions or coroutines. They receive the parsed
184
+ JSON payload of each incoming frame; errors in the callback are logged and
185
+ swallowed so one bad frame doesn't kill the stream.
186
+
187
+ ### Searching for instruments (securities)
188
+
189
+ `search_instrument(query, instrument_type=None, limit=20)` queries TR's
190
+ `neonSearch` endpoint — the same one the web app uses for the asset
191
+ picker. It accepts a free-text query (name, ticker, ISIN fragment) and
192
+ returns the raw result list.
193
+
194
+ ```python
195
+ async with client.open_session(session_token) as session:
196
+ # By asset class
197
+ btc = await session.search_instrument("bitcoin", instrument_type="crypto")
198
+ apple = await session.search_instrument("apple", instrument_type="stock")
199
+ msci = await session.search_instrument("MSCI World", instrument_type="etf")
200
+
201
+ # Without a type filter, results span all asset classes
202
+ mixed = await session.search_instrument("tesla", limit=5)
203
+
204
+ # By ISIN (or fragment)
205
+ by_isin = await session.search_instrument("US0378331005")
206
+ ```
207
+
208
+ **Parameters**
209
+
210
+ | Name | Type | Notes |
211
+ |---|---|---|
212
+ | `query` | `str` | Free-text search — name, ticker, partial ISIN |
213
+ | `instrument_type` | `str \| None` | One of `"crypto"`, `"stock"`, `"etf"`, `"bond"`, `"derivative"`, `"fund"` — or `None` to search everything |
214
+ | `limit` | `int` | Max results (default `20`) |
215
+
216
+ **Result shape** — each item is a raw TR dict, typically including:
217
+
218
+ ```python
219
+ {
220
+ "isin": "XF000BTC0017", # ISIN or pseudo-ISIN for crypto
221
+ "name": "Bitcoin",
222
+ "type": "crypto",
223
+ "exchanges": [{"slug": "BTC", "name": "Bitcoin"}],
224
+ # ...additional fields vary by asset class
225
+ }
226
+ ```
227
+
228
+ Use the returned `isin` to feed `subscribe_ticker()`, `fetch_ticker()`,
229
+ or any other instrument-keyed API on the client.
230
+
231
+ ### Lower-level primitives
232
+
233
+ If a TR subscription type isn't covered by the convenience helpers, use
234
+ `subscribe()` / `request()` directly:
235
+
236
+ ```python
237
+ async with client.open_session(session_token) as session:
238
+ # One-shot: subscribe, take the first frame, unsubscribe.
239
+ data = await session.request("availableCash", {"id": cash_acc_no})
240
+
241
+ # Streaming: keep receiving until you unsubscribe.
242
+ sub_id = await session.subscribe(
243
+ "compactPortfolio",
244
+ {"secAccNo": sec_acc_no},
245
+ callback=lambda d: print(d["positions"]),
246
+ )
247
+ ```
248
+
249
+ The WebSocket token is injected for you; you don't need to pass it in
250
+ `params`.
251
+
252
+ ## Downloading files listed in transactions
253
+
254
+ The key points:
255
+ - Cookie: tr_session=<session_token> — this is how TR authenticates document downloads (same mechanism as login).
256
+ - Headers: reuse client._headers() for x-aws-waf-token and x-tr-device-info — TR's WAF will reject requests without a valid token.
257
+ - The document URLs are absolute https:// URLs already, no base URL manipulation needed.
258
+
259
+ Examples:
260
+
261
+ ```python
262
+ import requests
263
+
264
+ def download_tr_documents(client, session_token: str, transactions: list, output_dir: str = "."):
265
+ """Download all PDF documents from a list of dual-legged transactions."""
266
+ import os
267
+
268
+ headers = client._headers() # includes x-aws-waf-token + x-tr-device-info
269
+ cookies = {"tr_session": session_token}
270
+
271
+ for tx in transactions:
272
+ for doc in tx.get("document_urls", []):
273
+ url = doc["url"]
274
+ title = doc["title"].replace("/", "-")
275
+ tr_id = tx.get("tr_id", "unknown")
276
+ filename = f"{tx['date'][:10]}_{tr_id}_{title}.pdf"
277
+ filepath = os.path.join(output_dir, filename)
278
+
279
+ resp = requests.get(url, headers=headers, cookies=cookies)
280
+ resp.raise_for_status()
281
+
282
+ with open(filepath, "wb") as f:
283
+ f.write(resp.content)
284
+ print(f"Saved: {filepath}")
285
+
286
+ result = asyncio.run(client.fetch_transactions(session_token))
287
+ download_tr_documents(client, session_token, result["transactions"], output_dir="/tmp/tr_docs")
288
+ ```
289
+
290
+ Or if you want to pull from raw_items instead (same document URLs, accessible before the dual-legged mapping):
291
+ ```
292
+ for item in result["raw_items"]:
293
+ for doc in item["_detail"].get("document_urls", []):
294
+ ...
295
+ ```
296
+
297
+ ## Pure parsing utilities
298
+
299
+ These are dependency-free helpers you can use without authenticating:
300
+
301
+ ```python
302
+ from traderepublic_sync import (
303
+ parse_currency_amount, # "1 000,00 EUR" → 1000.0
304
+ parse_detail_sections, # timelineDetailV2 dict → structured dict
305
+ extract_isin_from_icon, # "logos/FR0011550672/v2" → "FR0011550672"
306
+ )
307
+ ```
308
+
309
+ ## Layout
310
+
311
+ ```
312
+ src/traderepublic_sync/
313
+ ├── client.py # TRClient (login, 2FA, one-shot websocket fetches)
314
+ ├── session.py # TRSession (long-lived ws, callback-based subscriptions)
315
+ ├── waf.py # AWS WAF token via Playwright or Selenium
316
+ ├── parsing.py # parse_currency_amount, parse_detail_sections, ISIN extraction
317
+ ├── state.py # ConnectionState dataclass
318
+ ├── constants.py # API URLs, default headers, WS connect payload
319
+ ├── exceptions.py # TRAuthError
320
+ └── dual_legged/ # Optional dual-legged transaction mapping
321
+ └── mapping.py
322
+ ```
323
+
324
+ ## Reporting bugs
325
+
326
+ The TR API is undocumented and locale-sensitive — the most useful thing
327
+ you can attach to a bug report is a redacted copy of your own dump, so we
328
+ can reproduce the parsing path that misbehaved. Two scripts make this
329
+ safe:
330
+
331
+ 1. **Dump everything** with `examples/smoke_fetch_all.py` — writes
332
+ `accounts.json`, `assets.json`, `transactions_raw.json` and
333
+ `transactions_dl.json` under `examples/out/`. The raw file contains
334
+ the full TR responses (`_detail` + `_detail_raw`) which is what we
335
+ need for parser fixes.
336
+ 2. **Anonymize** the whole folder with `scripts/redact_dump.py` before
337
+ sharing. It keeps every item and preserves the data shape, but:
338
+ - replaces ``sender`` / ``iban`` / ``holderName`` / ``email`` /
339
+ ``phone`` field values wholesale,
340
+ - regex-scrubs IBANs, JWTs, emails, phone numbers, AWS pre-signed
341
+ URL query strings, and any 10+ digit run,
342
+ - maps TR cash account numbers to consistent placeholders
343
+ (``9000000001``, ``9000000002``, …) so cross-file references still
344
+ match,
345
+ - takes ``--also-redact "<string>"`` for anything the regexes can't
346
+ infer (your real name and its variants, account labels, etc.).
347
+ Matched case-insensitively. Repeat the flag per term.
348
+
349
+ ```bash
350
+ # Default in/out: examples/out/ → examples/out_redacted/
351
+ python scripts/redact_dump.py \
352
+ --also-redact "Jane Doe" \
353
+ --also-redact "Jane-Doe" \
354
+ --also-redact "DOE-J"
355
+ ```
356
+
357
+ The script prints a per-rule hit count and the cash-account-number
358
+ mapping at the end. **Open the redacted JSONs and search for any
359
+ remaining real names, IBAN fragments, or labels** — name variants
360
+ (truncations, capitalizations, hyphenations) are the classic blind
361
+ spot. Re-run with extra ``--also-redact`` flags until clean.
362
+
363
+ 3. **Open an issue** on
364
+ [github.com/hdecreis/libtrsync](https://github.com/hdecreis/libtrsync/issues)
365
+ describing what you expected vs. what happened, and attach the
366
+ relevant file(s) from `examples/out_redacted/`. A single offending
367
+ `eventType` is often enough — pulling one item with
368
+ `scripts/extract_fixture.py` (which also sanitizes) lets us turn it
369
+ straight into a regression test.
370
+
371
+ ## License
372
+
373
+ MIT. See [LICENSE](LICENSE).