-
-
Notifications
You must be signed in to change notification settings - Fork 159
Allow overriding the TR app version and User-Agent from the environment #382
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 3 commits
2dc8456
29cf365
fbaf091
375d703
a8f8cba
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -86,11 +86,32 @@ | |||||
| # The web frontend's API client identifies itself with this platform on all v2 login calls. | ||||||
| WEB_PLATFORM = "web-pro" | ||||||
|
|
||||||
| DEFAULT_USER_AGENT = ( | ||||||
| "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36" | ||||||
| ) | ||||||
|
|
||||||
| # All three values above describe someone else's deployment, and Trade Republic can | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the comment here in the code is too verbose, given that we more or less write the same in README.md. I suggest to make less words here. |
||||||
| # invalidate any of them at any moment: a frontend release makes APP_VERSION stale | ||||||
| # (the endpoints then answer 426 CLIENT_VERSION_OUTDATED, which is what #250 was), | ||||||
| # a change to their bot filtering can make the User-Agent the thing being rejected, | ||||||
| # and WEB_PLATFORM is whatever string their API client happens to be configured with. | ||||||
| # None of those failures needs a code change to fix, only a different string, so all | ||||||
| # three are readable from the environment. That turns "wait for a pytr release" into | ||||||
| # "export a variable" for a user who is locked out today. | ||||||
| # | ||||||
| # PYTR_TR_APP_VERSION=2.2700.4 pytr login --v2 | ||||||
| # PYTR_TR_USER_AGENT='Mozilla/5.0 ... Chrome/149.0.0.0 Safari/537.36' pytr login | ||||||
| # PYTR_TR_PLATFORM=web pytr login --v2 | ||||||
| # | ||||||
| # An unset or empty variable keeps the built-in default, so an empty assignment can | ||||||
| # never send an empty header. | ||||||
| ENV_APP_VERSION = "PYTR_TR_APP_VERSION" | ||||||
| ENV_USER_AGENT = "PYTR_TR_USER_AGENT" | ||||||
| ENV_PLATFORM = "PYTR_TR_PLATFORM" | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please sort the variables like above, e.g. ENV_APP_VERSION, ENV_PLATFORM, ENV_USER_AGENT |
||||||
|
|
||||||
|
|
||||||
| class TradeRepublicApi: | ||||||
| _default_headers = { | ||||||
| "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36" | ||||||
| } | ||||||
| _default_headers = {"User-Agent": DEFAULT_USER_AGENT} | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Initialize _default_headers using the environment in the central place, no? |
||||||
| _host = "https://api.traderepublic.com" | ||||||
| _waf_login_url = "https://app.traderepublic.com/login" | ||||||
|
|
||||||
|
|
@@ -144,6 +165,12 @@ def __init__( | |||||
| self._cookies_file = pathlib.Path(cookies_file) if cookies_file else BASE_DIR / f"cookies.{self.phone_no}.txt" | ||||||
|
|
||||||
| self._websession = requests.Session() | ||||||
| # Copy before overriding: `_default_headers` is a class attribute, and it is | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you follow my suggestion to initialize _default_headers using ENV_USER_AGENT, this whole block is not necessary. Or is there any other reason to clone the dict here for usage? |
||||||
| # handed straight to the session below, which mutates what it is given. | ||||||
| self._default_headers = dict(self._default_headers) | ||||||
| user_agent = os.environ.get(ENV_USER_AGENT) | ||||||
| if user_agent: | ||||||
| self._default_headers["User-Agent"] = user_agent | ||||||
| self._websession.headers = self._default_headers | ||||||
| if self._save_cookies: | ||||||
| self._websession.cookies = MozillaCookieJar(self._cookies_file) | ||||||
|
|
@@ -324,8 +351,8 @@ def _login_headers(self): | |||||
| self._device_info = base64.b64encode(json.dumps(device).encode()).decode() | ||||||
| return { | ||||||
| "X-TR-Device-Info": self._device_info, | ||||||
| "X-TR-App-Version": APP_VERSION, | ||||||
| "X-Tr-Platform": WEB_PLATFORM, | ||||||
| "X-TR-App-Version": os.environ.get(ENV_APP_VERSION) or APP_VERSION, | ||||||
| "X-Tr-Platform": os.environ.get(ENV_PLATFORM) or WEB_PLATFORM, | ||||||
| "Accept-Language": self._locale, | ||||||
| } | ||||||
|
|
||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,7 +8,15 @@ | |
| import pytest | ||
| import requests | ||
|
|
||
| from pytr.api import TradeRepublicApi | ||
| from pytr.api import ( | ||
| APP_VERSION, | ||
| DEFAULT_USER_AGENT, | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please also fix the ordering here in the sense of APP_VERSION, PLATFORM, USER_AGENT |
||
| ENV_APP_VERSION, | ||
| ENV_PLATFORM, | ||
| ENV_USER_AGENT, | ||
| WEB_PLATFORM, | ||
| TradeRepublicApi, | ||
| ) | ||
|
|
||
| LOGIN = "https://api.traderepublic.com/api/v2/auth/web/login" | ||
| PROCESS = "https://api.traderepublic.com/api/v2/auth/web/login/processes/pid-1" | ||
|
|
@@ -261,6 +269,75 @@ def test_app_version_and_platform_come_from_the_web_frontend(): | |
| assert headers["X-Tr-Platform"] == "web-pro" | ||
|
|
||
|
|
||
| # --- environment overrides ----------------------------------------------------------- | ||
|
|
||
| CHROME_149 = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36" | ||
|
|
||
|
|
||
| def _real_session_api(): | ||
| """An instance keeping its real session, so constructor-set headers survive.""" | ||
| return TradeRepublicApi(phone_no="+490000000000", pin="0000", waf_token=None, use_v2_login=True) | ||
|
|
||
|
|
||
| def test_app_version_can_be_overridden_from_the_environment(monkeypatch): | ||
| """A frontend release makes the built-in stale; 426 must be fixable without a release.""" | ||
| monkeypatch.setenv(ENV_APP_VERSION, "2.9999.1") | ||
|
|
||
| assert _api([])._login_headers()["X-TR-App-Version"] == "2.9999.1" | ||
|
|
||
|
|
||
| def test_user_agent_can_be_overridden_from_the_environment(monkeypatch): | ||
| monkeypatch.setenv(ENV_USER_AGENT, CHROME_149) | ||
|
|
||
| assert _real_session_api()._websession.headers["User-Agent"] == CHROME_149 | ||
|
|
||
|
|
||
| def test_platform_can_be_overridden_from_the_environment(monkeypatch): | ||
| monkeypatch.setenv(ENV_PLATFORM, "web") | ||
|
|
||
| assert _api([])._login_headers()["X-Tr-Platform"] == "web" | ||
|
|
||
|
|
||
| def test_an_empty_override_keeps_the_built_in_default(monkeypatch): | ||
| """An empty assignment must not send an empty header for any of the three.""" | ||
| monkeypatch.setenv(ENV_APP_VERSION, "") | ||
| monkeypatch.setenv(ENV_USER_AGENT, "") | ||
| monkeypatch.setenv(ENV_PLATFORM, "") | ||
|
|
||
| headers = _api([])._login_headers() | ||
| assert headers["X-TR-App-Version"] == APP_VERSION | ||
| assert headers["X-Tr-Platform"] == WEB_PLATFORM | ||
| assert _real_session_api()._websession.headers["User-Agent"] == DEFAULT_USER_AGENT | ||
|
|
||
|
|
||
| def test_overriding_the_user_agent_does_not_leak_into_other_instances(monkeypatch): | ||
| """The default lives on the class; overriding it must stay on the instance.""" | ||
| monkeypatch.setenv(ENV_USER_AGENT, CHROME_149) | ||
| _real_session_api() | ||
| monkeypatch.delenv(ENV_USER_AGENT) | ||
|
|
||
| assert TradeRepublicApi._default_headers["User-Agent"] == DEFAULT_USER_AGENT | ||
| assert _real_session_api()._websession.headers["User-Agent"] == DEFAULT_USER_AGENT | ||
|
|
||
|
|
||
| def test_device_info_follows_the_overridden_user_agent(monkeypatch): | ||
| """browserVersion is scraped from the User-Agent; the two must not drift apart.""" | ||
| monkeypatch.setenv(ENV_USER_AGENT, CHROME_149) | ||
|
|
||
| device = jsonlib.loads(base64.b64decode(_real_session_api()._login_headers()["X-TR-Device-Info"])) | ||
|
|
||
| assert device["browserVersion"] == "149.0.0.0" | ||
|
|
||
|
|
||
| def test_a_non_chrome_user_agent_leaves_the_browser_version_empty(monkeypatch): | ||
| """The frontend omits what the browser does not provide, and TR accepts that.""" | ||
| monkeypatch.setenv(ENV_USER_AGENT, "Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0") | ||
|
|
||
| device = jsonlib.loads(base64.b64decode(_real_session_api()._login_headers()["X-TR-Device-Info"])) | ||
|
|
||
| assert device["browserVersion"] == "" | ||
|
|
||
|
|
||
| # --- endpoints that must NOT move ---------------------------------------------------- | ||
|
|
||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we should just name it
USER_AGENTwhich is more inline with the namesAPP_VERSIONandWEB_PLATFORMabove.