From e339fa44f7ab16daf830e84c8d618a84558e3bed Mon Sep 17 00:00:00 2001 From: Marcus Date: Thu, 16 Jan 2025 18:40:58 +0100 Subject: [PATCH 1/3] add option to query account details including personal data --- pytr/accountdetails.py | 16 ++++++++++++++++ pytr/api.py | 7 +++++++ pytr/main.py | 20 ++++++++++++++++++++ 3 files changed, 43 insertions(+) create mode 100644 pytr/accountdetails.py diff --git a/pytr/accountdetails.py b/pytr/accountdetails.py new file mode 100644 index 00000000..773c8b28 --- /dev/null +++ b/pytr/accountdetails.py @@ -0,0 +1,16 @@ +import json + +class Accountdetails: + def __init__(self, tr): + self.tr = tr + self.data = None + + def get(self): + self.data = self.tr.get_account_details() + print(f"{self.data}") + return self.data + + def data_to_file(self, output_path): + with open(output_path, "w", encoding="utf-8") as f: + json.dump(self.data, f) + diff --git a/pytr/api.py b/pytr/api.py index ae471c76..ab4faa15 100644 --- a/pytr/api.py +++ b/pytr/api.py @@ -262,6 +262,13 @@ def resume_websession(self): return True return False + def get_account_details(self): + r = self._websession.get( + f"{self._host}/api/v2/auth/account", + ) + j = r.json() + return j + def _web_request(self, url_path, payload=None, method="GET"): if self._web_session_token_expires_at < time.time(): r = self._websession.get(f"{self._host}/api/v1/auth/web/session") diff --git a/pytr/main.py b/pytr/main.py index 8d80eda7..148599c2 100644 --- a/pytr/main.py +++ b/pytr/main.py @@ -17,6 +17,7 @@ from pytr.portfolio import Portfolio from pytr.alarms import Alarms from pytr.details import Details +from pytr.accountdetails import Accountdetails def get_main_parser(): @@ -200,6 +201,18 @@ def formatter(prog): help='Two letter language code or "auto" for system language', default="auto", ) + # account details + info = "Show account details" + parser_accountdetails = parser_cmd.add_parser( + "accountdetails", + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + parents=[parser_login_args], + help=info, + description=info, + ) + parser_accountdetails.add_argument( + "-j", "--jsonoutput", help="Output path of JSON file", metavar="OUTPUT", type=Path + ) info = "Print shell tab completion" parser_completion = parser_cmd.add_parser( @@ -287,6 +300,13 @@ def main(): installed_version = version("pytr") print(installed_version) check_version(installed_version) + elif args.command == "accountdetails": + ad = Accountdetails( + login(phone_no=args.phone_no, pin=args.pin, web=not args.applogin), + ) + ad.get() + if args.jsonoutput is not None: + ad.data_to_file(args.jsonoutput) else: parser.print_help() From beb62ff3e80186def1195a8d6e4d0c9b7e6c4563 Mon Sep 17 00:00:00 2001 From: Marcus Date: Sat, 15 Feb 2025 11:35:04 +0100 Subject: [PATCH 2/3] print/save account_details using JSON functions only --- pytr/main.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/pytr/main.py b/pytr/main.py index 73c55f00..9114e9f1 100644 --- a/pytr/main.py +++ b/pytr/main.py @@ -3,6 +3,7 @@ import argparse import asyncio import signal +import json from datetime import datetime, timedelta from importlib.metadata import version from pathlib import Path @@ -12,7 +13,6 @@ from pytr.account import login from pytr.alarms import Alarms from pytr.details import Details -from pytr.accountdetails import Accountdetails from pytr.dl import DL from pytr.portfolio import Portfolio from pytr.transactions import export_transactions @@ -209,7 +209,12 @@ def formatter(prog): description=info, ) parser_accountdetails.add_argument( - "-j", "--jsonoutput", help="Output path of JSON file", metavar="OUTPUT", type=Pathmaster + "-o", + "--outfile", + help='Output path of JSON file. [default: "-" (stdout)]', + metavar="PATH", + type=argparse.FileType("w"), + default="-", ) # completion info = "Print shell tab completion" @@ -324,12 +329,9 @@ def main(): print(installed_version) check_version(installed_version) elif args.command == "accountdetails": - ad = Accountdetails( - login(phone_no=args.phone_no, pin=args.pin, web=not args.applogin), - ) - ad.get() - if args.jsonoutput is not None: - ad.data_to_file(args.jsonoutput) + tr = login(phone_no=args.phone_no, pin=args.pin, web=not args.applogin) + account_details = tr.get_account_details() + args.jsonoutput.write(json.dumps(account_details)) else: parser.print_help() From 1ce3145f1864e53843b02b14f5d2043e607c1053 Mon Sep 17 00:00:00 2001 From: Marcus Date: Sat, 15 Feb 2025 11:42:36 +0100 Subject: [PATCH 3/3] AccountDetails as TypedDict with small test --- pytr/accountdetails.py | 109 ++++++++++++++++++++++++++---- tests/sample_account_details.json | 37 ++++++++++ tests/test_account_details.py | 17 +++++ 3 files changed, 150 insertions(+), 13 deletions(-) create mode 100644 tests/sample_account_details.json create mode 100644 tests/test_account_details.py diff --git a/pytr/accountdetails.py b/pytr/accountdetails.py index 773c8b28..cbcdf0ca 100644 --- a/pytr/accountdetails.py +++ b/pytr/accountdetails.py @@ -1,16 +1,99 @@ -import json +from typing import TypedDict, Optional, List +from typing import cast -class Accountdetails: - def __init__(self, tr): - self.tr = tr - self.data = None +class Name(TypedDict): + first: str + last: str - def get(self): - self.data = self.tr.get_account_details() - print(f"{self.data}") - return self.data - - def data_to_file(self, output_path): - with open(output_path, "w", encoding="utf-8") as f: - json.dump(self.data, f) +class Email(TypedDict): + address: str + verified: bool + +class PostalAddress(TypedDict): + street: str + houseNo: str + zip: str + city: str + country: str + +class Birthplace(TypedDict): + birthplace: str + birthcountry: str + +class TaxResidency(TypedDict): + tin: str + countryCode: str + +class TaxExemptionOrder(TypedDict): + minimum: int + maximum: int + current: int + applied: int + syncStatus: int + validFrom: int + validUntil: int + +class CashAccount(TypedDict): + iban: str + bic: str + bankName: str + logoUrl: Optional[str] + +class ReferenceAccount(TypedDict): + iban: str + bic: Optional[str] + bankName: Optional[str] + logoUrl: Optional[str] + +class Experience(TypedDict): + tradeCount: int + level: str + showsRiskWarning: bool + +class InvestmentExperience(TypedDict): + stock: Experience + fund: Experience + derivative: Experience + crypto: Experience + bond: Experience + +class SupportDocuments(TypedDict): + accountClosing: str + imprint: str + addressConfirmation: str + +class TinFormat(TypedDict): + placeholder: str + keyboardLayout: str + +class AccountDetails(TypedDict): + phoneNumber: str + jurisdiction: str + name: Name + email: Email + duplicateTradingEmail: Optional[str] + postalAddress: PostalAddress + birthdate: str + birthplace: Birthplace + mainNationality: str + additionalNationalities: List[str] + mainTaxResidency: TaxResidency + usTaxResidency: bool + additionalTaxResidencies: List[TaxResidency] + taxInformationSyncTimestamp: int + taxExemptionOrder: TaxExemptionOrder + registrationAccount: bool + cashAccount: CashAccount + referenceAccount: ReferenceAccount + referenceAccountV2: Optional[str] + referenceAccountList: List[ReferenceAccount] + securitiesAccountNumber: str + experience: InvestmentExperience + referralDetails: Optional[str] + supportDocuments: SupportDocuments + tinFormat: TinFormat + personId: str + def from_dict(d): + account_details: AccountDetails = cast(AccountDetails, d) + return account_details diff --git a/tests/sample_account_details.json b/tests/sample_account_details.json new file mode 100644 index 00000000..bb0f75b3 --- /dev/null +++ b/tests/sample_account_details.json @@ -0,0 +1,37 @@ +{ + "phoneNumber":"+491xxxx", + "jurisdiction":"DE", + "name":{"first":"xxfirstnamexx", "last":"xxx"}, + "email":{"address":"xxxx@xxxx.de", "verified":true}, + "duplicateTradingEmail":null, + "postalAddress":{"street":"SomeStreet", "houseNo":"4", "zip":"123456", "city":"Somecity", "country":"DE"}, + "birthdate":"1902-01-01", + "birthplace":{"birthplace":"Somecity", "birthcountry":"DE"}, + "mainNationality":"DE", + "additionalNationalities":[], + "mainTaxResidency":{"tin":"12345987", "countryCode":"DE"}, + "usTaxResidency":false, + "additionalTaxResidencies":[], + "taxInformationSyncTimestamp":172177300000, + "taxExemptionOrder":{"minimum":0, "maximum":9900, "current":111, "applied":2131, "syncStatus":12414, "validFrom":1242, "validUntil":2312}, + "registrationAccount":false, + "cashAccount":{"iban":"DE00000000000000000000", "bic":"TRBKDEBBXXX", "bankName":"J.P. Morgan SE", "logoUrl":null}, + "referenceAccount":{"iban":"-", "bic":null}, + "referenceAccountV2":null, + "referenceAccountList":[{"iban":"DE00012134874564878747", "bic":null, "bankName":"Bank", "logoUrl":"logos/bank_bank/v2"}], + "securitiesAccountNumber":"321354657", + "experience":{"stock":{"tradeCount":0, "level":"LOSER", "showsRiskWarning":true}, + "fund":{"tradeCount":0, "level":"LOSER", "showsRiskWarning":true}, + "derivative":{"tradeCount":0, "level":"LOSER", "showsRiskWarning":true}, + "crypto":{"tradeCount":100, "level":"GODMODE", "showsRiskWarning":true}, + "bond":{"level":"xxxx", "showsRiskWarning":true}}, + "referralDetails":null, + "supportDocuments":{ + "accountClosing":"https://assets.traderepublic.com/assets/files/foilename.pdf", + "imprint":"https://traderepublic.com/de-de/imprint", + "addressConfirmation":"https://support.traderepublic.com/de-de/102"}, + "tinFormat":{"placeholder":"99999999999", + "keyboardLayout":"numerical" + }, + "personId":"babdbddb-5441-23-124-423423542532112" +} \ No newline at end of file diff --git a/tests/test_account_details.py b/tests/test_account_details.py new file mode 100644 index 00000000..ad99abf7 --- /dev/null +++ b/tests/test_account_details.py @@ -0,0 +1,17 @@ +import json + +from pytr.accountdetails import AccountDetails + + +def test_account_details_from_dict(): + # Load the sample JSON file + with open("tests/sample_account_details.json", "r") as file: + sample_data = json.load(file) + + # Parse the JSON data using the from_dict function + ad = AccountDetails.from_dict(sample_data) + + # Assert the expected values + assert ad["phoneNumber"] == "+491xxxx" + assert ad["name"]["first"] == "xxfirstnamexx" + assert ad["taxExemptionOrder"]["current"] == 111