ctrader-api-client 0.7.1__tar.gz → 0.7.2__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.
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/.gitignore +4 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/PKG-INFO +1 -1
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/docs/api/events.md +16 -4
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/pyproject.toml +1 -1
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/auth/_recovery.py +39 -15
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/auth/_refresh.py +7 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/auth/_session.py +38 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/auth/manager.py +39 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/tests/behavior/auth/test_token_lifecycle.py +117 -17
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/tests/behavior/test_client.py +8 -2
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/tests/harness/factories.py +10 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/uv.lock +1 -1
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/.claude/settings.local.json +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/.github/workflows/docs.yml +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/.pre-commit-config.yaml +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/.python-version +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/Justfile +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/LICENSE +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/README.md +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/docs/api/accounts.md +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/docs/api/client.md +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/docs/api/enums.md +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/docs/api/exceptions.md +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/docs/api/market-data.md +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/docs/api/models.md +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/docs/api/symbols.md +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/docs/api/trading.md +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/docs/getting-started.md +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/docs/index.md +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/mkdocs.yml +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/protos/SOURCE +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/protos/VERSION +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/protos/update.sh +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/protos/vendor/OpenApiCommonMessages.proto +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/protos/vendor/OpenApiCommonModelMessages.proto +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/protos/vendor/OpenApiMessages.proto +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/protos/vendor/OpenApiModelMessages.proto +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/scripts/fix_proto_imports.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/__init__.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/_internal/__init__.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/_internal/clock.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/_internal/conversions.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/_internal/messages.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/_internal/proto/OpenApiCommonMessages.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/_internal/proto/OpenApiCommonModelMessages.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/_internal/proto/OpenApiMessages.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/_internal/proto/OpenApiModelMessages.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/_internal/proto/__init__.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/_internal/serialization.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/api/__init__.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/api/_base.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/api/accounts.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/api/market_data.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/api/symbols.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/api/trading.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/auth/__init__.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/auth/credentials.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/auth/policy.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/auth/store.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/client.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/composition.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/config.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/connection/__init__.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/connection/heartbeat.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/connection/listener.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/connection/protocol.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/connection/supervisor.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/connection/transport.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/enums.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/events/__init__.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/events/_execution.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/events/emitter.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/events/publisher.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/events/router.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/events/types.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/exceptions.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/models/__init__.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/models/_base.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/models/account.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/models/deal.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/models/market_data.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/models/order.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/models/position.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/models/requests.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/models/symbol.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/py.typed +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/tests/__init__.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/tests/behavior/__init__.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/tests/behavior/api/__init__.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/tests/behavior/api/conftest.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/tests/behavior/api/test_market_data.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/tests/behavior/api/test_reference_data.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/tests/behavior/api/test_subscription_restore.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/tests/behavior/api/test_trading.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/tests/behavior/auth/__init__.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/tests/behavior/auth/conftest.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/tests/behavior/auth/test_authentication.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/tests/behavior/connection/__init__.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/tests/behavior/connection/conftest.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/tests/behavior/connection/test_event_dispatch.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/tests/behavior/connection/test_framing.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/tests/behavior/connection/test_heartbeat.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/tests/behavior/connection/test_reconnection.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/tests/behavior/connection/test_request_response.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/tests/behavior/events/__init__.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/tests/behavior/events/conftest.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/tests/behavior/events/test_routing.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/tests/behavior/events/test_subscriptions.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/tests/conftest.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/tests/harness/__init__.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/tests/harness/clock.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/tests/harness/recorder.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/tests/harness/server.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/tests/harness/signals.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/tests/harness/stub_protocol.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/tests/harness/wire.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/tests/unit/__init__.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/tests/unit/test_api_errors.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/tests/unit/test_market_data_conversion.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/tests/unit/test_request_conversion.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/tests/unit/test_symbol_conversion.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/tests/unit/test_trading_records.py +0 -0
- {ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/tests/unit/test_wire_format.py +0 -0
|
@@ -161,10 +161,22 @@ drop. Check current authorization with `client.is_account_authorized(account_id)
|
|
|
161
161
|
|
|
162
162
|
A disconnect the server reports is checked before it is published. Rotating an
|
|
163
163
|
access token makes the server report one for a session it has not ended, so the
|
|
164
|
-
client
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
164
|
+
client asks whether the account is still served on this connection: if it is,
|
|
165
|
+
nothing is published. The check does not re-authenticate, because that answers
|
|
166
|
+
for the token as much as for the session, and the rotation prompting the report
|
|
167
|
+
is the very thing that would make such an answer unreliable.
|
|
168
|
+
|
|
169
|
+
The check also waits for any token refresh on that account to finish first.
|
|
170
|
+
Rotating a token means the server drops the old authorization and the client
|
|
171
|
+
establishes a new one, and in between the account is genuinely unauthorized — it
|
|
172
|
+
refuses everything, including the check. Since the refresh always begins before
|
|
173
|
+
the report it provokes, waiting for it to settle is what keeps a moment the
|
|
174
|
+
client itself created from reading as a lost session.
|
|
175
|
+
|
|
176
|
+
A check that cannot be completed — a timeout, a link that has just gone — is not
|
|
177
|
+
read as a disconnect either. The account is left as it was, and the next report
|
|
178
|
+
is checked afresh. What reaches a handler is therefore always a session the
|
|
179
|
+
server confirmed it was no longer serving.
|
|
168
180
|
|
|
169
181
|
::: ctrader_api_client.events.TokenInvalidatedEvent
|
|
170
182
|
options:
|
{ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/auth/_recovery.py
RENAMED
|
@@ -104,14 +104,32 @@ class SessionRecovery:
|
|
|
104
104
|
unauthorized and publish a drop that never occurred, so the report is
|
|
105
105
|
checked against the server before anything acts on it.
|
|
106
106
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
107
|
+
Every report the server sends for a token rotation arrives while the
|
|
108
|
+
client is replacing that authorization, and for the length of that
|
|
109
|
+
replacement the account really is unauthorized: it answers nothing, and
|
|
110
|
+
asking it anything would only confirm a state the client is already
|
|
111
|
+
undoing. So the check waits for the refresh to settle before it asks.
|
|
112
|
+
The refresh always starts before the report it provokes, so there is no
|
|
113
|
+
ordering in which the report escapes the wait.
|
|
114
|
+
|
|
115
|
+
The check itself asks whether the account is still served on this link
|
|
116
|
+
rather than trying to re-authorize it, since re-authorizing answers for
|
|
117
|
+
the token as much as for the session.
|
|
118
|
+
|
|
119
|
+
A check that cannot be completed is not an answer of no. The account is
|
|
120
|
+
left as it was, because nothing has said the session ended.
|
|
111
121
|
|
|
112
122
|
Args:
|
|
113
123
|
account_id: The cTID trader account ID reported as disconnected.
|
|
114
124
|
"""
|
|
125
|
+
if self._store.is_refreshing(account_id):
|
|
126
|
+
logger.debug(
|
|
127
|
+
"Account %d was reported as disconnected while its token was being "
|
|
128
|
+
"refreshed; waiting for that to settle before checking",
|
|
129
|
+
account_id,
|
|
130
|
+
)
|
|
131
|
+
await self._store.wait_for_refresh(account_id)
|
|
132
|
+
|
|
115
133
|
credentials = self._store.credentials(account_id)
|
|
116
134
|
if credentials is None or not self._store.is_authorized(account_id):
|
|
117
135
|
return
|
|
@@ -121,30 +139,36 @@ class SessionRecovery:
|
|
|
121
139
|
|
|
122
140
|
self._verifying.add(account_id)
|
|
123
141
|
try:
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
142
|
+
try:
|
|
143
|
+
alive = await self._authenticator.probe_session(account_id)
|
|
144
|
+
except Exception as e:
|
|
145
|
+
logger.debug(
|
|
146
|
+
"Could not check the disconnect reported for account %d, leaving it as it is: %s",
|
|
147
|
+
account_id,
|
|
148
|
+
e,
|
|
149
|
+
)
|
|
150
|
+
return
|
|
151
|
+
|
|
152
|
+
if alive:
|
|
127
153
|
logger.debug(
|
|
128
154
|
"Account %d reported as disconnected still holds its session; ignoring",
|
|
129
155
|
account_id,
|
|
130
156
|
)
|
|
131
157
|
return
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
await self._report_dropped(account_id, e)
|
|
158
|
+
|
|
159
|
+
await self._report_dropped(account_id)
|
|
135
160
|
finally:
|
|
136
161
|
self._verifying.discard(account_id)
|
|
137
162
|
|
|
138
|
-
async def _report_dropped(self, account_id: int
|
|
163
|
+
async def _report_dropped(self, account_id: int) -> None:
|
|
139
164
|
"""Publish a confirmed drop and arm recovery for it.
|
|
140
165
|
|
|
141
|
-
Reached once the server has
|
|
142
|
-
session is genuinely gone
|
|
166
|
+
Reached once the server has stopped serving the account on this link, so
|
|
167
|
+
the session is genuinely gone.
|
|
143
168
|
"""
|
|
144
169
|
logger.warning(
|
|
145
|
-
"Account %d disconnected by server; scheduling re-authentication
|
|
170
|
+
"Account %d disconnected by server; scheduling re-authentication",
|
|
146
171
|
account_id,
|
|
147
|
-
error,
|
|
148
172
|
)
|
|
149
173
|
self._store.flag_for_recovery(account_id)
|
|
150
174
|
self._reauth_signal.set()
|
{ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/auth/_refresh.py
RENAMED
|
@@ -142,6 +142,11 @@ class TokenRefresher:
|
|
|
142
142
|
|
|
143
143
|
last_error: Exception | None = None
|
|
144
144
|
|
|
145
|
+
# Held from before the request goes out until the account is authorized
|
|
146
|
+
# again, because the disconnect the server reports for the old token
|
|
147
|
+
# arrives inside that window and means nothing about the session.
|
|
148
|
+
self._store.begin_refresh(account_id)
|
|
149
|
+
|
|
145
150
|
try:
|
|
146
151
|
async for attempt in AsyncRetrying(
|
|
147
152
|
stop=stop_after_attempt(self._policy.retry_attempts),
|
|
@@ -198,6 +203,8 @@ class TokenRefresher:
|
|
|
198
203
|
last_error = e
|
|
199
204
|
except Exception as e:
|
|
200
205
|
last_error = e
|
|
206
|
+
finally:
|
|
207
|
+
self._store.end_refresh(account_id)
|
|
201
208
|
|
|
202
209
|
if last_error is not None:
|
|
203
210
|
raise TokenRefreshError(account_id, last_error)
|
{ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/auth/_session.py
RENAMED
|
@@ -17,6 +17,8 @@ from __future__ import annotations
|
|
|
17
17
|
from dataclasses import dataclass
|
|
18
18
|
from typing import Protocol as TypingProtocol
|
|
19
19
|
|
|
20
|
+
import anyio
|
|
21
|
+
|
|
20
22
|
from ..enums import AuthTrigger
|
|
21
23
|
from .credentials import AccountCredentials
|
|
22
24
|
|
|
@@ -77,6 +79,15 @@ class SessionAuthenticator(TypingProtocol):
|
|
|
77
79
|
"""Authorize an account, recording why so the right events follow."""
|
|
78
80
|
...
|
|
79
81
|
|
|
82
|
+
async def probe_session(self, account_id: int, timeout: float = 10.0) -> bool:
|
|
83
|
+
"""Whether the account still holds a live session on this link.
|
|
84
|
+
|
|
85
|
+
Raises whatever stopped the question from being answered — including a
|
|
86
|
+
refusal that is about the token rather than the session — which is not
|
|
87
|
+
the same as an answer of no.
|
|
88
|
+
"""
|
|
89
|
+
...
|
|
90
|
+
|
|
80
91
|
|
|
81
92
|
class SessionStore:
|
|
82
93
|
"""Every account the client knows about, keyed by cTID trader account ID.
|
|
@@ -90,6 +101,33 @@ class SessionStore:
|
|
|
90
101
|
def __init__(self) -> None:
|
|
91
102
|
self._accounts: dict[int, TrackedAccount] = {}
|
|
92
103
|
|
|
104
|
+
# Accounts whose authorization is being replaced by a token refresh.
|
|
105
|
+
# The server drops the old authorization as it issues the new pair, so
|
|
106
|
+
# for that stretch the account is genuinely unauthorized and answers
|
|
107
|
+
# nothing — a state no observer should mistake for a lost session.
|
|
108
|
+
self._refreshing: dict[int, anyio.Event] = {}
|
|
109
|
+
|
|
110
|
+
def begin_refresh(self, account_id: int) -> None:
|
|
111
|
+
"""Note that the account's authorization is being replaced."""
|
|
112
|
+
if account_id not in self._refreshing:
|
|
113
|
+
self._refreshing[account_id] = anyio.Event()
|
|
114
|
+
|
|
115
|
+
def end_refresh(self, account_id: int) -> None:
|
|
116
|
+
"""Note that it has been replaced, or that the attempt gave up."""
|
|
117
|
+
settled = self._refreshing.pop(account_id, None)
|
|
118
|
+
if settled is not None:
|
|
119
|
+
settled.set()
|
|
120
|
+
|
|
121
|
+
def is_refreshing(self, account_id: int) -> bool:
|
|
122
|
+
"""Whether a refresh is currently replacing the account's authorization."""
|
|
123
|
+
return account_id in self._refreshing
|
|
124
|
+
|
|
125
|
+
async def wait_for_refresh(self, account_id: int) -> None:
|
|
126
|
+
"""Wait until no refresh is replacing the account's authorization."""
|
|
127
|
+
settled = self._refreshing.get(account_id)
|
|
128
|
+
if settled is not None:
|
|
129
|
+
await settled.wait()
|
|
130
|
+
|
|
93
131
|
def account_ids(self) -> list[int]:
|
|
94
132
|
"""Every account held, oldest first."""
|
|
95
133
|
return list(self._accounts)
|
{ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/auth/manager.py
RENAMED
|
@@ -15,6 +15,8 @@ from .._internal.proto import (
|
|
|
15
15
|
ProtoOAAccountAuthRes,
|
|
16
16
|
ProtoOAApplicationAuthReq,
|
|
17
17
|
ProtoOAApplicationAuthRes,
|
|
18
|
+
ProtoOATraderReq,
|
|
19
|
+
ProtoOATraderRes,
|
|
18
20
|
)
|
|
19
21
|
from ..enums import AuthTrigger
|
|
20
22
|
from ..events import EventPublisher, ReadyEvent
|
|
@@ -238,6 +240,43 @@ class AuthManager:
|
|
|
238
240
|
await self._restorer.restore(credentials.account_id)
|
|
239
241
|
await self._publisher.emit(ReadyEvent(account_id=credentials.account_id, trigger=trigger))
|
|
240
242
|
|
|
243
|
+
async def probe_session(self, account_id: int, timeout: float = 10.0) -> bool:
|
|
244
|
+
"""Whether the account still holds a live session on this link.
|
|
245
|
+
|
|
246
|
+
Asks the server for something only an authorized account is told. The
|
|
247
|
+
request carries no access token of its own, but the authorization behind
|
|
248
|
+
it does: while a rotation is in flight the server refuses everything for
|
|
249
|
+
the account, including this. That refusal names the token, and it is
|
|
250
|
+
raised rather than answered, because it says nothing about the session.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
account_id: The cTID trader account ID.
|
|
254
|
+
timeout: Request timeout in seconds.
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
True if the server still serves the account on this link.
|
|
258
|
+
|
|
259
|
+
Raises:
|
|
260
|
+
APIError: If the refusal is about the token rather than the account.
|
|
261
|
+
CTraderConnectionTimeoutError: If the request times out.
|
|
262
|
+
CTraderConnectionClosedError: If the link is gone.
|
|
263
|
+
"""
|
|
264
|
+
request = ProtoOATraderReq(ctid_trader_account_id=account_id)
|
|
265
|
+
|
|
266
|
+
try:
|
|
267
|
+
await self._protocol.request(request, ProtoOATraderRes, timeout=timeout)
|
|
268
|
+
except APIError as e:
|
|
269
|
+
logger.debug(
|
|
270
|
+
"Account %d was refused a trader lookup: %s (%s)",
|
|
271
|
+
account_id,
|
|
272
|
+
e.error_code,
|
|
273
|
+
e.description,
|
|
274
|
+
)
|
|
275
|
+
if e.is_token_failure():
|
|
276
|
+
raise
|
|
277
|
+
return False
|
|
278
|
+
return True
|
|
279
|
+
|
|
241
280
|
def remove_account(self, account_id: int) -> bool:
|
|
242
281
|
"""Remove an account from refresh monitoring.
|
|
243
282
|
|
{ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/tests/behavior/auth/test_token_lifecycle.py
RENAMED
|
@@ -4,14 +4,19 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
from collections.abc import Callable
|
|
6
6
|
|
|
7
|
+
import anyio
|
|
7
8
|
import betterproto
|
|
8
9
|
import pytest
|
|
9
10
|
|
|
10
|
-
from ctrader_api_client._internal.proto import
|
|
11
|
+
from ctrader_api_client._internal.proto import (
|
|
12
|
+
ProtoOAAccountAuthReq,
|
|
13
|
+
ProtoOARefreshTokenReq,
|
|
14
|
+
ProtoOATraderReq,
|
|
15
|
+
)
|
|
11
16
|
from ctrader_api_client.auth import AuthManager, ReauthPolicy, SessionRecovery, SessionStore
|
|
12
17
|
from ctrader_api_client.enums import AuthTrigger
|
|
13
18
|
from ctrader_api_client.events import AccountDisconnectEvent, ReadyEvent, TokenRefreshFailedEvent
|
|
14
|
-
from ctrader_api_client.exceptions import APIError, TokenExpiredError
|
|
19
|
+
from ctrader_api_client.exceptions import APIError, CTraderConnectionTimeoutError, TokenExpiredError
|
|
15
20
|
|
|
16
21
|
from ...harness import ManualClock, RecordingPublisher, RecordingStore, StubProtocol, factories
|
|
17
22
|
from .conftest import Monitors
|
|
@@ -304,6 +309,7 @@ async def test_an_account_the_server_confirms_is_gone_stops_being_authorized(
|
|
|
304
309
|
ProtoOAAccountAuthReq,
|
|
305
310
|
[factories.account_auth_res(), APIError(error_code="TEMPORARY")],
|
|
306
311
|
)
|
|
312
|
+
protocol.respond(ProtoOATraderReq, APIError(error_code="ACCOUNT_NOT_AUTHORIZED"))
|
|
307
313
|
await auth.authenticate_trader(factories.credentials(expires_in=PLENTY_OF_TIME))
|
|
308
314
|
await clock.wait_for_sleepers(SLEEPERS_WHEN_STARTED)
|
|
309
315
|
|
|
@@ -325,14 +331,11 @@ async def test_a_session_the_server_still_holds_is_left_alone(
|
|
|
325
331
|
Regression guard: taking that report at face value marked a working account
|
|
326
332
|
unauthorized, published a drop that never happened, and left recovery
|
|
327
333
|
retrying an account the server would only ever refuse as already logged in.
|
|
334
|
+
Checking it by re-authorizing was no better, because the rotation that
|
|
335
|
+
prompts the report is what makes that check answer wrongly.
|
|
328
336
|
"""
|
|
329
|
-
protocol.
|
|
330
|
-
|
|
331
|
-
[
|
|
332
|
-
factories.account_auth_res(),
|
|
333
|
-
APIError(error_code="ALREADY_LOGGED_IN", description="Trading account is already authorized"),
|
|
334
|
-
],
|
|
335
|
-
)
|
|
337
|
+
protocol.respond(ProtoOAAccountAuthReq, factories.account_auth_res())
|
|
338
|
+
protocol.respond(ProtoOATraderReq, factories.trader_res())
|
|
336
339
|
await auth.authenticate_trader(factories.credentials(expires_in=PLENTY_OF_TIME))
|
|
337
340
|
await clock.wait_for_sleepers(SLEEPERS_WHEN_STARTED)
|
|
338
341
|
|
|
@@ -341,6 +344,98 @@ async def test_a_session_the_server_still_holds_is_left_alone(
|
|
|
341
344
|
assert auth.is_account_authorized(factories.ACCOUNT_ID)
|
|
342
345
|
assert publisher.of(AccountDisconnectEvent) == []
|
|
343
346
|
assert publisher.of(ReadyEvent) == [ReadyEvent(factories.ACCOUNT_ID, AuthTrigger.INITIAL)]
|
|
347
|
+
# Nothing re-authorized the account, so nothing could have disturbed the
|
|
348
|
+
# session the report was wrong about.
|
|
349
|
+
assert len(protocol.sent_of(ProtoOAAccountAuthReq)) == 1
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
async def test_a_check_that_cannot_be_answered_leaves_the_account_alone(
|
|
353
|
+
auth: AuthManager,
|
|
354
|
+
monitors: Monitors,
|
|
355
|
+
protocol: StubProtocol,
|
|
356
|
+
clock: ManualClock,
|
|
357
|
+
publisher: RecordingPublisher,
|
|
358
|
+
) -> None:
|
|
359
|
+
"""Failing to ask is not the same as being told the session is gone.
|
|
360
|
+
|
|
361
|
+
Regression guard: anything that stopped the check from completing used to
|
|
362
|
+
count as confirmation, so a report that could not be checked took a working
|
|
363
|
+
account out of service.
|
|
364
|
+
"""
|
|
365
|
+
protocol.respond(ProtoOAAccountAuthReq, factories.account_auth_res())
|
|
366
|
+
protocol.respond(ProtoOATraderReq, CTraderConnectionTimeoutError(10.0, "request"))
|
|
367
|
+
await auth.authenticate_trader(factories.credentials(expires_in=PLENTY_OF_TIME))
|
|
368
|
+
await clock.wait_for_sleepers(SLEEPERS_WHEN_STARTED)
|
|
369
|
+
|
|
370
|
+
await monitors.recovery.handle_account_disconnect(factories.ACCOUNT_ID)
|
|
371
|
+
|
|
372
|
+
assert auth.is_account_authorized(factories.ACCOUNT_ID)
|
|
373
|
+
assert publisher.of(AccountDisconnectEvent) == []
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
async def test_a_disconnect_reported_while_the_token_rotates_is_not_a_drop(
|
|
377
|
+
auth: AuthManager,
|
|
378
|
+
monitors: Monitors,
|
|
379
|
+
protocol: StubProtocol,
|
|
380
|
+
clock: ManualClock,
|
|
381
|
+
publisher: RecordingPublisher,
|
|
382
|
+
sessions: SessionStore,
|
|
383
|
+
) -> None:
|
|
384
|
+
"""The window a refresh opens is not a session anyone lost.
|
|
385
|
+
|
|
386
|
+
Regression guard: the server reports a disconnect for the authorization a
|
|
387
|
+
rotation replaces, and while the replacement is in flight the account is
|
|
388
|
+
genuinely unauthorized — it refuses everything with INVALID_REQUEST. Asking
|
|
389
|
+
it anything in that window and believing the answer published a drop for a
|
|
390
|
+
session that was serving requests a moment later.
|
|
391
|
+
"""
|
|
392
|
+
protocol.respond(ProtoOAAccountAuthReq, factories.account_auth_res())
|
|
393
|
+
protocol.respond(ProtoOATraderReq, APIError(error_code="INVALID_REQUEST"))
|
|
394
|
+
await auth.authenticate_trader(factories.credentials(expires_in=PLENTY_OF_TIME))
|
|
395
|
+
await clock.wait_for_sleepers(SLEEPERS_WHEN_STARTED)
|
|
396
|
+
sessions.begin_refresh(factories.ACCOUNT_ID)
|
|
397
|
+
|
|
398
|
+
async with anyio.create_task_group() as task_group:
|
|
399
|
+
task_group.start_soon(monitors.recovery.handle_account_disconnect, factories.ACCOUNT_ID)
|
|
400
|
+
await anyio.sleep(0.01)
|
|
401
|
+
# Nothing has been decided while the rotation is in flight.
|
|
402
|
+
assert publisher.of(AccountDisconnectEvent) == []
|
|
403
|
+
assert not protocol.sent_of(ProtoOATraderReq)
|
|
404
|
+
|
|
405
|
+
protocol.respond(ProtoOATraderReq, factories.trader_res())
|
|
406
|
+
sessions.end_refresh(factories.ACCOUNT_ID)
|
|
407
|
+
|
|
408
|
+
assert auth.is_account_authorized(factories.ACCOUNT_ID)
|
|
409
|
+
assert publisher.of(AccountDisconnectEvent) == []
|
|
410
|
+
assert len(protocol.sent_of(ProtoOATraderReq)) == 1
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
@pytest.mark.parametrize("error_code", ["OA_AUTH_TOKEN_EXPIRED", "CH_ACCESS_TOKEN_INVALID"])
|
|
414
|
+
async def test_a_check_refused_over_the_token_leaves_the_account_alone(
|
|
415
|
+
error_code: str,
|
|
416
|
+
auth: AuthManager,
|
|
417
|
+
monitors: Monitors,
|
|
418
|
+
protocol: StubProtocol,
|
|
419
|
+
clock: ManualClock,
|
|
420
|
+
publisher: RecordingPublisher,
|
|
421
|
+
) -> None:
|
|
422
|
+
"""A refusal naming the token says nothing about the session behind it.
|
|
423
|
+
|
|
424
|
+
Regression guard: the server refuses everything for an account while a
|
|
425
|
+
rotation is in flight, and the disconnect it reports for that same rotation
|
|
426
|
+
arrives inside the window. Reading the refusal as an answer published a drop
|
|
427
|
+
for a session that was still being served.
|
|
428
|
+
"""
|
|
429
|
+
protocol.respond(ProtoOAAccountAuthReq, factories.account_auth_res())
|
|
430
|
+
protocol.respond(ProtoOATraderReq, APIError(error_code=error_code))
|
|
431
|
+
await auth.authenticate_trader(factories.credentials(expires_in=PLENTY_OF_TIME))
|
|
432
|
+
await clock.wait_for_sleepers(SLEEPERS_WHEN_STARTED)
|
|
433
|
+
|
|
434
|
+
await monitors.recovery.handle_account_disconnect(factories.ACCOUNT_ID)
|
|
435
|
+
|
|
436
|
+
assert auth.is_account_authorized(factories.ACCOUNT_ID)
|
|
437
|
+
assert publisher.of(AccountDisconnectEvent) == []
|
|
438
|
+
assert len(protocol.sent_of(ProtoOAAccountAuthReq)) == 1
|
|
344
439
|
|
|
345
440
|
|
|
346
441
|
async def test_a_disconnected_account_is_re_authenticated(
|
|
@@ -351,6 +446,7 @@ async def test_a_disconnected_account_is_re_authenticated(
|
|
|
351
446
|
publisher: RecordingPublisher,
|
|
352
447
|
) -> None:
|
|
353
448
|
await start_with_account(auth, protocol, clock, expires_in=PLENTY_OF_TIME)
|
|
449
|
+
protocol.respond(ProtoOATraderReq, APIError(error_code="ACCOUNT_NOT_AUTHORIZED"))
|
|
354
450
|
|
|
355
451
|
await monitors.recovery.handle_account_disconnect(factories.ACCOUNT_ID)
|
|
356
452
|
await publisher.wait_for_type(ReadyEvent, count=2)
|
|
@@ -370,12 +466,13 @@ async def test_recovery_keeps_trying_after_a_failure(
|
|
|
370
466
|
ProtoOAAccountAuthReq,
|
|
371
467
|
[
|
|
372
468
|
factories.account_auth_res(),
|
|
373
|
-
# The
|
|
374
|
-
|
|
469
|
+
# The first recovery attempt after the drop is confirmed, then the
|
|
470
|
+
# retry that succeeds.
|
|
375
471
|
APIError(error_code="TEMPORARY"),
|
|
376
472
|
factories.account_auth_res(),
|
|
377
473
|
],
|
|
378
474
|
)
|
|
475
|
+
protocol.respond(ProtoOATraderReq, APIError(error_code="ACCOUNT_NOT_AUTHORIZED"))
|
|
379
476
|
await auth.authenticate_trader(factories.credentials(expires_in=PLENTY_OF_TIME))
|
|
380
477
|
await clock.wait_for_sleepers(SLEEPERS_WHEN_STARTED)
|
|
381
478
|
|
|
@@ -393,6 +490,7 @@ async def test_an_account_dropped_during_another_backoff_is_recovered_at_once(
|
|
|
393
490
|
make_monitors: Callable[..., Monitors],
|
|
394
491
|
protocol: StubProtocol,
|
|
395
492
|
clock: ManualClock,
|
|
493
|
+
publisher: RecordingPublisher,
|
|
396
494
|
) -> None:
|
|
397
495
|
"""Backoff is per account, so a healthy account never queues behind a sick one."""
|
|
398
496
|
stubborn = 111
|
|
@@ -411,12 +509,16 @@ async def test_an_account_dropped_during_another_backoff_is_recovered_at_once(
|
|
|
411
509
|
return factories.account_auth_res()
|
|
412
510
|
|
|
413
511
|
protocol.respond_with(ProtoOAAccountAuthReq, answer)
|
|
512
|
+
# Both reports are real: the server no longer serves either account.
|
|
513
|
+
protocol.respond(ProtoOATraderReq, APIError(error_code="ACCOUNT_NOT_AUTHORIZED"))
|
|
414
514
|
|
|
415
515
|
await monitors.recovery.handle_account_disconnect(stubborn)
|
|
416
516
|
# Its failed attempt parks recovery on a ten second backoff.
|
|
417
517
|
await clock.wait_for_sleepers(SLEEPERS_WHEN_STARTED + 1)
|
|
418
518
|
|
|
419
519
|
await monitors.recovery.handle_account_disconnect(healthy)
|
|
520
|
+
# Recovery re-authorizes it without waiting out the stubborn account's backoff.
|
|
521
|
+
await publisher.wait_for_type(ReadyEvent, count=3)
|
|
420
522
|
|
|
421
523
|
assert auth.is_account_authorized(healthy)
|
|
422
524
|
assert not auth.is_account_authorized(stubborn)
|
|
@@ -450,16 +552,14 @@ async def test_a_report_for_an_account_already_recovering_is_not_checked_again(
|
|
|
450
552
|
indistinguishable from a second check of the same report.
|
|
451
553
|
"""
|
|
452
554
|
recovery = SessionRecovery(store=sessions, authenticator=auth, publisher=publisher, clock=clock)
|
|
453
|
-
protocol.
|
|
454
|
-
|
|
455
|
-
[factories.account_auth_res(), APIError(error_code="TEMPORARY")],
|
|
456
|
-
)
|
|
555
|
+
protocol.respond(ProtoOAAccountAuthReq, factories.account_auth_res())
|
|
556
|
+
protocol.respond(ProtoOATraderReq, APIError(error_code="ACCOUNT_NOT_AUTHORIZED"))
|
|
457
557
|
await auth.authenticate_trader(factories.credentials(expires_in=PLENTY_OF_TIME))
|
|
458
558
|
|
|
459
559
|
await recovery.handle_account_disconnect(factories.ACCOUNT_ID)
|
|
460
|
-
checked = len(protocol.sent_of(
|
|
560
|
+
checked = len(protocol.sent_of(ProtoOATraderReq))
|
|
461
561
|
await recovery.handle_account_disconnect(factories.ACCOUNT_ID)
|
|
462
562
|
await recovery.handle_account_disconnect(factories.ACCOUNT_ID)
|
|
463
563
|
|
|
464
|
-
assert len(protocol.sent_of(
|
|
564
|
+
assert len(protocol.sent_of(ProtoOATraderReq)) == checked
|
|
465
565
|
assert publisher.of(AccountDisconnectEvent) == [AccountDisconnectEvent(factories.ACCOUNT_ID)]
|
|
@@ -14,6 +14,7 @@ from ctrader_api_client._internal.proto import (
|
|
|
14
14
|
ProtoOAAccountDisconnectEvent,
|
|
15
15
|
ProtoOAApplicationAuthReq,
|
|
16
16
|
ProtoOARefreshTokenReq,
|
|
17
|
+
ProtoOATraderReq,
|
|
17
18
|
)
|
|
18
19
|
from ctrader_api_client.auth import AccountCredentials
|
|
19
20
|
from ctrader_api_client.events import (
|
|
@@ -383,6 +384,8 @@ async def test_an_account_the_server_drops_is_re_authenticated(
|
|
|
383
384
|
await authenticate(client, server)
|
|
384
385
|
ready: Recorder[ReadyEvent] = Recorder()
|
|
385
386
|
client.register_handler(ReadyEvent, ready)
|
|
387
|
+
# The server stands behind the report: it will not answer for the account.
|
|
388
|
+
server.on(ProtoOATraderReq, rejecting)
|
|
386
389
|
|
|
387
390
|
await server.push(ProtoOAAccountDisconnectEvent(ctid_trader_account_id=factories.ACCOUNT_ID))
|
|
388
391
|
await ready.wait_for(1)
|
|
@@ -398,6 +401,7 @@ async def test_a_dropped_account_stays_unauthorized_until_recovery_succeeds(
|
|
|
398
401
|
await authenticate(client, server)
|
|
399
402
|
dropped: Recorder[AccountDisconnectEvent] = Recorder()
|
|
400
403
|
client.register_handler(AccountDisconnectEvent, dropped)
|
|
404
|
+
server.on(ProtoOATraderReq, rejecting)
|
|
401
405
|
server.on(ProtoOAAccountAuthReq, rejecting)
|
|
402
406
|
|
|
403
407
|
await server.push(ProtoOAAccountDisconnectEvent(ctid_trader_account_id=factories.ACCOUNT_ID))
|
|
@@ -419,14 +423,16 @@ async def test_a_disconnect_the_server_does_not_stand_behind_is_not_published(
|
|
|
419
423
|
ready: Recorder[ReadyEvent] = Recorder()
|
|
420
424
|
client.register_handler(AccountDisconnectEvent, dropped)
|
|
421
425
|
client.register_handler(ReadyEvent, ready)
|
|
422
|
-
server.
|
|
426
|
+
server.respond(ProtoOATraderReq, factories.trader_res())
|
|
423
427
|
|
|
424
428
|
await server.push(ProtoOAAccountDisconnectEvent(ctid_trader_account_id=factories.ACCOUNT_ID))
|
|
425
|
-
await server.wait_for_request(
|
|
429
|
+
await server.wait_for_request(ProtoOATraderReq)
|
|
426
430
|
|
|
427
431
|
assert dropped.count == 0
|
|
428
432
|
assert ready.count == 0
|
|
429
433
|
assert client.is_account_authorized(factories.ACCOUNT_ID) is True
|
|
434
|
+
# The account was never re-authorized, only asked after.
|
|
435
|
+
assert len(server.requests_of(ProtoOAAccountAuthReq)) == 1
|
|
430
436
|
|
|
431
437
|
|
|
432
438
|
# -----------------------------------------------------------------------------
|
|
@@ -16,6 +16,8 @@ from ctrader_api_client._internal.proto import (
|
|
|
16
16
|
ProtoOAGetAccountListByAccessTokenRes,
|
|
17
17
|
ProtoOARefreshTokenRes,
|
|
18
18
|
ProtoOASpotEvent,
|
|
19
|
+
ProtoOATrader,
|
|
20
|
+
ProtoOATraderRes,
|
|
19
21
|
ProtoOATrendbar,
|
|
20
22
|
)
|
|
21
23
|
from ctrader_api_client.auth import AccountCredentials
|
|
@@ -57,6 +59,14 @@ def account_auth_res(account_id: int = ACCOUNT_ID) -> ProtoOAAccountAuthRes:
|
|
|
57
59
|
return ProtoOAAccountAuthRes(ctid_trader_account_id=account_id)
|
|
58
60
|
|
|
59
61
|
|
|
62
|
+
def trader_res(account_id: int = ACCOUNT_ID) -> ProtoOATraderRes:
|
|
63
|
+
"""A successful trader lookup, which only an authorized account is given."""
|
|
64
|
+
return ProtoOATraderRes(
|
|
65
|
+
ctid_trader_account_id=account_id,
|
|
66
|
+
trader=ProtoOATrader(ctid_trader_account_id=account_id),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
60
70
|
def ctid_account(
|
|
61
71
|
account_id: int = ACCOUNT_ID,
|
|
62
72
|
trader_login: int = TRADER_LOGIN,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/protos/vendor/OpenApiCommonMessages.proto
RENAMED
|
File without changes
|
{ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/protos/vendor/OpenApiCommonModelMessages.proto
RENAMED
|
File without changes
|
|
File without changes
|
{ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/protos/vendor/OpenApiModelMessages.proto
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/_internal/__init__.py
RENAMED
|
File without changes
|
{ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/_internal/clock.py
RENAMED
|
File without changes
|
|
File without changes
|
{ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/_internal/messages.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/api/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/api/accounts.py
RENAMED
|
File without changes
|
{ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/api/market_data.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/auth/__init__.py
RENAMED
|
File without changes
|
{ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/auth/credentials.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/connection/__init__.py
RENAMED
|
File without changes
|
{ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/connection/heartbeat.py
RENAMED
|
File without changes
|
{ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/connection/listener.py
RENAMED
|
File without changes
|
{ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/connection/protocol.py
RENAMED
|
File without changes
|
|
File without changes
|
{ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/connection/transport.py
RENAMED
|
File without changes
|
|
File without changes
|
{ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/events/__init__.py
RENAMED
|
File without changes
|
{ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/events/_execution.py
RENAMED
|
File without changes
|
{ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/events/emitter.py
RENAMED
|
File without changes
|
{ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/events/publisher.py
RENAMED
|
File without changes
|
{ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/events/router.py
RENAMED
|
File without changes
|
{ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/events/types.py
RENAMED
|
File without changes
|
|
File without changes
|
{ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/models/__init__.py
RENAMED
|
File without changes
|
{ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/models/_base.py
RENAMED
|
File without changes
|
{ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/models/account.py
RENAMED
|
File without changes
|
|
File without changes
|
{ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/models/market_data.py
RENAMED
|
File without changes
|
{ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/models/order.py
RENAMED
|
File without changes
|
{ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/models/position.py
RENAMED
|
File without changes
|
{ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/models/requests.py
RENAMED
|
File without changes
|
{ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/src/ctrader_api_client/models/symbol.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/tests/behavior/api/test_market_data.py
RENAMED
|
File without changes
|
{ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/tests/behavior/api/test_reference_data.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/tests/behavior/auth/test_authentication.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/tests/behavior/connection/test_framing.py
RENAMED
|
File without changes
|
{ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/tests/behavior/connection/test_heartbeat.py
RENAMED
|
File without changes
|
{ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/tests/behavior/connection/test_reconnection.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/tests/behavior/events/test_subscriptions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ctrader_api_client-0.7.1 → ctrader_api_client-0.7.2}/tests/unit/test_market_data_conversion.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|