ctrader-api-client 0.7.2__tar.gz → 0.7.3__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.2 → ctrader_api_client-0.7.3}/PKG-INFO +1 -1
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/docs/api/events.md +8 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/pyproject.toml +2 -1
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/auth/_recovery.py +89 -3
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/connection/protocol.py +68 -23
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/connection/transport.py +1 -1
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/events/types.py +7 -1
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/tests/behavior/connection/test_reconnection.py +97 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/tests/behavior/test_client.py +78 -1
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/uv.lock +1 -1
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/.claude/settings.local.json +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/.github/workflows/docs.yml +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/.gitignore +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/.pre-commit-config.yaml +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/.python-version +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/Justfile +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/LICENSE +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/README.md +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/docs/api/accounts.md +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/docs/api/client.md +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/docs/api/enums.md +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/docs/api/exceptions.md +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/docs/api/market-data.md +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/docs/api/models.md +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/docs/api/symbols.md +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/docs/api/trading.md +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/docs/getting-started.md +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/docs/index.md +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/mkdocs.yml +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/protos/SOURCE +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/protos/VERSION +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/protos/update.sh +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/protos/vendor/OpenApiCommonMessages.proto +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/protos/vendor/OpenApiCommonModelMessages.proto +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/protos/vendor/OpenApiMessages.proto +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/protos/vendor/OpenApiModelMessages.proto +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/scripts/fix_proto_imports.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/__init__.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/_internal/__init__.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/_internal/clock.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/_internal/conversions.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/_internal/messages.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/_internal/proto/OpenApiCommonMessages.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/_internal/proto/OpenApiCommonModelMessages.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/_internal/proto/OpenApiMessages.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/_internal/proto/OpenApiModelMessages.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/_internal/proto/__init__.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/_internal/serialization.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/api/__init__.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/api/_base.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/api/accounts.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/api/market_data.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/api/symbols.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/api/trading.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/auth/__init__.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/auth/_refresh.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/auth/_session.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/auth/credentials.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/auth/manager.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/auth/policy.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/auth/store.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/client.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/composition.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/config.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/connection/__init__.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/connection/heartbeat.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/connection/listener.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/connection/supervisor.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/enums.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/events/__init__.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/events/_execution.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/events/emitter.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/events/publisher.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/events/router.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/exceptions.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/models/__init__.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/models/_base.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/models/account.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/models/deal.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/models/market_data.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/models/order.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/models/position.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/models/requests.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/models/symbol.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/py.typed +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/tests/__init__.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/tests/behavior/__init__.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/tests/behavior/api/__init__.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/tests/behavior/api/conftest.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/tests/behavior/api/test_market_data.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/tests/behavior/api/test_reference_data.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/tests/behavior/api/test_subscription_restore.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/tests/behavior/api/test_trading.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/tests/behavior/auth/__init__.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/tests/behavior/auth/conftest.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/tests/behavior/auth/test_authentication.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/tests/behavior/auth/test_token_lifecycle.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/tests/behavior/connection/__init__.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/tests/behavior/connection/conftest.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/tests/behavior/connection/test_event_dispatch.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/tests/behavior/connection/test_framing.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/tests/behavior/connection/test_heartbeat.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/tests/behavior/connection/test_request_response.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/tests/behavior/events/__init__.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/tests/behavior/events/conftest.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/tests/behavior/events/test_routing.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/tests/behavior/events/test_subscriptions.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/tests/conftest.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/tests/harness/__init__.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/tests/harness/clock.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/tests/harness/factories.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/tests/harness/recorder.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/tests/harness/server.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/tests/harness/signals.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/tests/harness/stub_protocol.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/tests/harness/wire.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/tests/unit/__init__.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/tests/unit/test_api_errors.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/tests/unit/test_market_data_conversion.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/tests/unit/test_request_conversion.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/tests/unit/test_symbol_conversion.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/tests/unit/test_trading_records.py +0 -0
- {ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/tests/unit/test_wire_format.py +0 -0
|
@@ -146,6 +146,14 @@ async def on_reconnected(event: ReconnectedEvent):
|
|
|
146
146
|
print(f"Failed accounts: {event.failed_accounts}")
|
|
147
147
|
```
|
|
148
148
|
|
|
149
|
+
An `app_auth_restored=False` is not the end of the story. The server can refuse
|
|
150
|
+
application authentication for transient reasons of its own, so the client keeps
|
|
151
|
+
retrying with backoff and announces each account it restores afterwards with a
|
|
152
|
+
`ReadyEvent`. Treat this event as a signal that the link is back but degraded —
|
|
153
|
+
stop acting on the session until the accounts are announced ready — rather than
|
|
154
|
+
as a permanent failure. Check the current state with
|
|
155
|
+
`client.auth.is_app_authenticated`.
|
|
156
|
+
|
|
149
157
|
::: ctrader_api_client.events.ClientDisconnectEvent
|
|
150
158
|
options:
|
|
151
159
|
show_source: false
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "ctrader-api-client"
|
|
3
|
-
version = "0.7.
|
|
3
|
+
version = "0.7.3"
|
|
4
4
|
description = "API Client to interact with the cTrader Open API spec"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
@@ -79,6 +79,7 @@ select = [
|
|
|
79
79
|
"RUF022", # Unsorted __all__ blocks
|
|
80
80
|
"FURB157", # Check unnecessary casts in Decimal constructors
|
|
81
81
|
"RUF032", # Check for Decimal calls passing a float literal
|
|
82
|
+
"G004", # Check for all f strings in logging calls
|
|
82
83
|
]
|
|
83
84
|
|
|
84
85
|
[tool.ruff.lint.per-file-ignores]
|
{ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/auth/_recovery.py
RENAMED
|
@@ -60,6 +60,14 @@ class SessionRecovery:
|
|
|
60
60
|
# repeated report does not start a second check against the server.
|
|
61
61
|
self._verifying: set[int] = set()
|
|
62
62
|
|
|
63
|
+
# A reconnect whose application re-authentication failed, left for the
|
|
64
|
+
# recovery loop to keep retrying. Nothing can be authorized on a link
|
|
65
|
+
# the application itself is not authenticated on, so this is settled
|
|
66
|
+
# before any account is attempted.
|
|
67
|
+
self._app_auth_pending = False
|
|
68
|
+
self._app_auth_attempts = 0
|
|
69
|
+
self._app_auth_next_at = 0.0
|
|
70
|
+
|
|
63
71
|
self._task_scope: anyio.CancelScope | None = None
|
|
64
72
|
self._running = False
|
|
65
73
|
|
|
@@ -85,6 +93,7 @@ class SessionRecovery:
|
|
|
85
93
|
finally:
|
|
86
94
|
self._running = False
|
|
87
95
|
self._task_scope = None
|
|
96
|
+
self._app_auth_pending = False
|
|
88
97
|
self._store.invalidate_all()
|
|
89
98
|
logger.debug("Session recovery monitor stopped")
|
|
90
99
|
|
|
@@ -181,6 +190,11 @@ class SessionRecovery:
|
|
|
181
190
|
credentials are kept, so the sessions can be re-established once the
|
|
182
191
|
link is back.
|
|
183
192
|
"""
|
|
193
|
+
# Any application re-authentication still being retried belonged to the
|
|
194
|
+
# link that just died. The reconnection attempts it again from scratch
|
|
195
|
+
# and re-arms this if it has to, so carrying the old state forward would
|
|
196
|
+
# only make the next backoff start deeper than it should.
|
|
197
|
+
self._app_auth_pending = False
|
|
184
198
|
self._store.invalidate_all()
|
|
185
199
|
|
|
186
200
|
async def on_connection_restored(self) -> None:
|
|
@@ -190,6 +204,11 @@ class SessionRecovery:
|
|
|
190
204
|
link is already back, and a caller can only act on a failure it is told
|
|
191
205
|
about. Without application authentication nothing else can be
|
|
192
206
|
attempted, so that failure is reported on its own.
|
|
207
|
+
|
|
208
|
+
Only the first attempt is made here. This runs inside the protocol's
|
|
209
|
+
reconnection task, which cannot detect a further drop while it is
|
|
210
|
+
blocked, so a failure is handed to the recovery loop to keep retrying in
|
|
211
|
+
the background rather than waited out inline.
|
|
193
212
|
"""
|
|
194
213
|
logger.debug("Connection restored, re-authenticating...")
|
|
195
214
|
|
|
@@ -197,7 +216,8 @@ class SessionRecovery:
|
|
|
197
216
|
await self._authenticator.authenticate_app()
|
|
198
217
|
logger.debug("App re-authenticated successfully")
|
|
199
218
|
except Exception as e:
|
|
200
|
-
logger.error("Failed to re-authenticate app after reconnect: %s", e)
|
|
219
|
+
logger.error("Failed to re-authenticate app after reconnect, will retry: %s", e)
|
|
220
|
+
self._arm_app_auth_recovery()
|
|
201
221
|
await self._publisher.emit(
|
|
202
222
|
ReconnectedEvent(
|
|
203
223
|
app_auth_restored=False,
|
|
@@ -228,15 +248,66 @@ class SessionRecovery:
|
|
|
228
248
|
)
|
|
229
249
|
)
|
|
230
250
|
|
|
251
|
+
def _backoff_delay(self, attempts: int) -> float:
|
|
252
|
+
"""How long to wait before attempt number `attempts`, per the policy."""
|
|
253
|
+
return min(self._policy.min_wait * 2 ** (attempts - 1), self._policy.max_wait)
|
|
254
|
+
|
|
231
255
|
def _defer_recovery(self, account_id: int, state: AwaitingRecovery) -> None:
|
|
232
256
|
"""Back off before the next recovery attempt for a single account."""
|
|
233
257
|
attempts = state.attempts + 1
|
|
234
|
-
delay = min(self._policy.min_wait * 2 ** (attempts - 1), self._policy.max_wait)
|
|
235
258
|
self._store.reschedule_recovery(
|
|
236
259
|
account_id,
|
|
237
|
-
AwaitingRecovery(
|
|
260
|
+
AwaitingRecovery(
|
|
261
|
+
attempts=attempts,
|
|
262
|
+
next_attempt_at=self._clock.now() + self._backoff_delay(attempts),
|
|
263
|
+
),
|
|
238
264
|
)
|
|
239
265
|
|
|
266
|
+
def _arm_app_auth_recovery(self) -> None:
|
|
267
|
+
"""Hand a failed application re-authentication to the recovery loop.
|
|
268
|
+
|
|
269
|
+
Every account is flagged alongside it: their sessions died with the
|
|
270
|
+
link, and the re-authentication that would have restored them is the
|
|
271
|
+
very thing that failed, so they are recovered by the same loop once the
|
|
272
|
+
application is authenticated again.
|
|
273
|
+
"""
|
|
274
|
+
self._app_auth_pending = True
|
|
275
|
+
self._app_auth_attempts = 0
|
|
276
|
+
self._defer_app_auth()
|
|
277
|
+
|
|
278
|
+
for credentials in self._store.all_credentials():
|
|
279
|
+
self._store.flag_for_recovery(credentials.account_id)
|
|
280
|
+
|
|
281
|
+
self._reauth_signal.set()
|
|
282
|
+
|
|
283
|
+
def _defer_app_auth(self) -> None:
|
|
284
|
+
"""Back off before the next application re-authentication attempt."""
|
|
285
|
+
self._app_auth_attempts += 1
|
|
286
|
+
self._app_auth_next_at = self._clock.now() + self._backoff_delay(self._app_auth_attempts)
|
|
287
|
+
|
|
288
|
+
async def _retry_app_auth(self) -> bool:
|
|
289
|
+
"""Attempt the deferred application re-authentication once.
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
True if the application is authenticated again, False if the
|
|
293
|
+
attempt failed and has been deferred for another try.
|
|
294
|
+
"""
|
|
295
|
+
try:
|
|
296
|
+
await self._authenticator.authenticate_app()
|
|
297
|
+
except Exception as e:
|
|
298
|
+
self._defer_app_auth()
|
|
299
|
+
logger.warning(
|
|
300
|
+
"Re-authenticating the application failed (attempt %d), will retry: %s",
|
|
301
|
+
self._app_auth_attempts,
|
|
302
|
+
e,
|
|
303
|
+
)
|
|
304
|
+
return False
|
|
305
|
+
|
|
306
|
+
self._app_auth_pending = False
|
|
307
|
+
self._app_auth_attempts = 0
|
|
308
|
+
logger.info("Application re-authenticated; restoring accounts")
|
|
309
|
+
return True
|
|
310
|
+
|
|
240
311
|
async def _wait_for_retry(self, delay: float, flagged: anyio.Event) -> None:
|
|
241
312
|
"""Wait out a backoff, returning early if another account needs recovery.
|
|
242
313
|
|
|
@@ -266,6 +337,11 @@ class SessionRecovery:
|
|
|
266
337
|
and retries continue until every account succeeds, is removed, or the
|
|
267
338
|
monitor stops. A successful re-auth restores authorized state and emits
|
|
268
339
|
a ReadyEvent so subscriptions can be restored.
|
|
340
|
+
|
|
341
|
+
Also carries the application re-authentication a reconnect could not
|
|
342
|
+
complete. That one is not per-account and blocks every account behind
|
|
343
|
+
it, so it is settled first and the accounts are left flagged until it
|
|
344
|
+
succeeds.
|
|
269
345
|
"""
|
|
270
346
|
while self._running:
|
|
271
347
|
# Consume any pending notification before reading the queue, so a
|
|
@@ -274,6 +350,16 @@ class SessionRecovery:
|
|
|
274
350
|
self._reauth_signal = anyio.Event()
|
|
275
351
|
flagged = self._reauth_signal
|
|
276
352
|
|
|
353
|
+
if self._app_auth_pending:
|
|
354
|
+
waiting = self._app_auth_next_at - self._clock.now()
|
|
355
|
+
if waiting > 0:
|
|
356
|
+
await self._wait_for_retry(waiting, flagged)
|
|
357
|
+
continue
|
|
358
|
+
if not await self._retry_app_auth():
|
|
359
|
+
continue
|
|
360
|
+
# Authenticating the application is exactly what the flagged
|
|
361
|
+
# accounts were held up by, so go straight on to them.
|
|
362
|
+
|
|
277
363
|
queue = self._store.awaiting_recovery()
|
|
278
364
|
if not queue:
|
|
279
365
|
await flagged.wait()
|
{ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/connection/protocol.py
RENAMED
|
@@ -109,6 +109,11 @@ class Protocol:
|
|
|
109
109
|
# reader loop and the heartbeat loop both detecting the same drop).
|
|
110
110
|
self._reconnecting = False
|
|
111
111
|
|
|
112
|
+
# Set when the link drops again while a reconnection is already in
|
|
113
|
+
# flight. The detector that saw it cannot start its own reconnection,
|
|
114
|
+
# so the one already running goes round again on its behalf.
|
|
115
|
+
self._redo_requested = False
|
|
116
|
+
|
|
112
117
|
# Told about link transitions; set by whoever owns this protocol.
|
|
113
118
|
self._listener: ConnectionListener | None = None
|
|
114
119
|
|
|
@@ -444,11 +449,17 @@ class Protocol:
|
|
|
444
449
|
standalone task decouples the reconnection from the cancel scope of
|
|
445
450
|
whichever loop detected the drop.
|
|
446
451
|
|
|
447
|
-
Idempotent: calls made while a reconnection is already in flight
|
|
448
|
-
|
|
449
|
-
drop only produce a single reconnection.
|
|
452
|
+
Idempotent: calls made while a reconnection is already in flight do not
|
|
453
|
+
start a second one, so the reader and heartbeat loops racing to report
|
|
454
|
+
the same drop only produce a single reconnection. Such a call is still
|
|
455
|
+
recorded, because it may be reporting a *fresh* drop of the link the
|
|
456
|
+
running reconnection just restored — and the caller cannot retry for
|
|
457
|
+
itself, since the reader loop exits as soon as it has reported.
|
|
450
458
|
"""
|
|
451
|
-
if not self._running
|
|
459
|
+
if not self._running:
|
|
460
|
+
return
|
|
461
|
+
if self._reconnecting:
|
|
462
|
+
self._redo_requested = True
|
|
452
463
|
return
|
|
453
464
|
if self._task_group is None:
|
|
454
465
|
return
|
|
@@ -457,16 +468,25 @@ class Protocol:
|
|
|
457
468
|
logger.warning("Connection lost, attempting to reconnect...")
|
|
458
469
|
|
|
459
470
|
# State tied to the dead link is invalid from this moment on. Report it
|
|
460
|
-
# before reconnecting so nothing observes a session that no longer
|
|
461
|
-
|
|
462
|
-
if self._listener is not None:
|
|
463
|
-
try:
|
|
464
|
-
await self._listener.on_connection_lost()
|
|
465
|
-
except Exception:
|
|
466
|
-
logger.exception("Connection listener failed on disconnect")
|
|
471
|
+
# before reconnecting so nothing observes a session that no longer exists.
|
|
472
|
+
await self._notify_connection_lost()
|
|
467
473
|
|
|
468
474
|
self._task_group.start_soon(self._reconnect_task)
|
|
469
475
|
|
|
476
|
+
async def _notify_connection_lost(self) -> None:
|
|
477
|
+
"""Tell the listener the link is gone.
|
|
478
|
+
|
|
479
|
+
Never lets a failing listener block the reconnection: whatever
|
|
480
|
+
bookkeeping it does matters less than getting the link back.
|
|
481
|
+
"""
|
|
482
|
+
if self._listener is None:
|
|
483
|
+
return
|
|
484
|
+
|
|
485
|
+
try:
|
|
486
|
+
await self._listener.on_connection_lost()
|
|
487
|
+
except Exception:
|
|
488
|
+
logger.exception("Connection listener failed on disconnect")
|
|
489
|
+
|
|
470
490
|
async def _reconnect_task(self) -> None:
|
|
471
491
|
"""Own the full reconnection lifecycle from a standalone task.
|
|
472
492
|
|
|
@@ -475,20 +495,44 @@ class Protocol:
|
|
|
475
495
|
escape: an unhandled error here would propagate into the protocol task
|
|
476
496
|
group and tear down every other task. A terminal reconnection failure is
|
|
477
497
|
instead recorded by stopping the protocol and waking pending requests.
|
|
498
|
+
|
|
499
|
+
Loops rather than running once, so a link that drops again while this is
|
|
500
|
+
working — reported through `handle_disconnect` as `_redo_requested`, the
|
|
501
|
+
only route left once the reader has exited — is reconnected too instead
|
|
502
|
+
of being stranded.
|
|
478
503
|
"""
|
|
479
504
|
try:
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
505
|
+
while True:
|
|
506
|
+
self._redo_requested = False
|
|
507
|
+
|
|
508
|
+
# The reader loop is reading a dead stream; cancel it before we
|
|
509
|
+
# reconnect so a stale reader can't race the fresh one we restart.
|
|
510
|
+
if self._reader_scope is not None:
|
|
511
|
+
self._reader_scope.cancel()
|
|
512
|
+
self._reader_scope = None
|
|
513
|
+
|
|
514
|
+
# Close the old transport (idempotent — the task that detected
|
|
515
|
+
# the drop may have already raced us to it). Tearing down a dead
|
|
516
|
+
# socket can fail on its own, which says nothing about whether
|
|
517
|
+
# reconnecting will work: swallow it here so it can never be
|
|
518
|
+
# mistaken below for an exhausted reconnection.
|
|
519
|
+
try:
|
|
520
|
+
await self._transport.close()
|
|
521
|
+
except Exception:
|
|
522
|
+
logger.debug("Ignoring error while closing dead transport", exc_info=True)
|
|
523
|
+
|
|
524
|
+
await self._reconnect()
|
|
525
|
+
logger.info("Reconnection successful")
|
|
526
|
+
|
|
527
|
+
if not self._redo_requested:
|
|
528
|
+
break
|
|
529
|
+
|
|
530
|
+
logger.warning("Connection lost again while reconnecting, retrying...")
|
|
531
|
+
# The sessions this pass just restored died with the link that
|
|
532
|
+
# dropped underneath it, so report that loss before going round
|
|
533
|
+
# again — nothing else will, since `handle_disconnect` only
|
|
534
|
+
# recorded the drop rather than handling it.
|
|
535
|
+
await self._notify_connection_lost()
|
|
492
536
|
except (CTraderConnectionFailedError, CTraderConnectionClosedError) as e:
|
|
493
537
|
logger.error("Reconnection failed, giving up: %s", e)
|
|
494
538
|
self._running = False
|
|
@@ -501,6 +545,7 @@ class Protocol:
|
|
|
501
545
|
self._fail_pending("Reconnection error")
|
|
502
546
|
finally:
|
|
503
547
|
self._reconnecting = False
|
|
548
|
+
self._redo_requested = False
|
|
504
549
|
|
|
505
550
|
def _fail_pending(self, reason: str) -> None:
|
|
506
551
|
"""Resolve every in-flight request with a connection error."""
|
{ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/connection/transport.py
RENAMED
|
@@ -104,7 +104,7 @@ class Transport:
|
|
|
104
104
|
await stream.aclose()
|
|
105
105
|
if close_scope.cancelled_caught:
|
|
106
106
|
logger.debug("Graceful TLS shutdown timed out, forcing close")
|
|
107
|
-
except (OSError, anyio.ClosedResourceError):
|
|
107
|
+
except (OSError, anyio.ClosedResourceError, anyio.BrokenResourceError):
|
|
108
108
|
pass
|
|
109
109
|
|
|
110
110
|
async def send(self, data: bytes) -> None:
|
{ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/events/types.py
RENAMED
|
@@ -324,8 +324,14 @@ class ReconnectedEvent:
|
|
|
324
324
|
Use this event for any custom logic that depends on reconnection, such as logging or alerting.
|
|
325
325
|
|
|
326
326
|
Attributes:
|
|
327
|
-
app_auth_restored: Whether app authentication succeeded
|
|
327
|
+
app_auth_restored: Whether app authentication succeeded on the attempt
|
|
328
|
+
this event reports. False is not final: the client keeps retrying
|
|
329
|
+
with backoff, and announces each account it restores afterwards with
|
|
330
|
+
a ReadyEvent. Check `client.auth.is_app_authenticated` for the
|
|
331
|
+
state now.
|
|
328
332
|
restored_accounts: Account IDs that were successfully re-authenticated.
|
|
333
|
+
Empty when app authentication failed, since nothing can be
|
|
334
|
+
authorized before it succeeds — not a report that they were lost.
|
|
329
335
|
failed_accounts: Accounts that failed, as (account_id, error_message) tuples.
|
|
330
336
|
"""
|
|
331
337
|
|
{ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/tests/behavior/connection/test_reconnection.py
RENAMED
|
@@ -15,6 +15,7 @@ from ctrader_api_client._internal.proto import (
|
|
|
15
15
|
ProtoOATraderRes,
|
|
16
16
|
)
|
|
17
17
|
from ctrader_api_client.composition import ClientGraph
|
|
18
|
+
from ctrader_api_client.connection import Transport
|
|
18
19
|
from ctrader_api_client.exceptions import CTraderConnectionClosedError
|
|
19
20
|
|
|
20
21
|
from ...harness import FakeServer, Recorder, factories
|
|
@@ -109,6 +110,102 @@ async def test_reconnection_restores_event_delivery(
|
|
|
109
110
|
assert spots.only.bid == 111_111
|
|
110
111
|
|
|
111
112
|
|
|
113
|
+
async def test_closing_a_transport_whose_socket_is_broken_does_not_raise() -> None:
|
|
114
|
+
"""A socket that refuses a graceful shutdown is still going away.
|
|
115
|
+
|
|
116
|
+
A broken TLS session raises `BrokenResourceError` out of `aclose()`, because
|
|
117
|
+
the close-notify it tries to write cannot be sent. That is the expected
|
|
118
|
+
ending for a link that is already dead, not a failure to report.
|
|
119
|
+
"""
|
|
120
|
+
transport = Transport(host="stub.invalid", port=0, use_ssl=False)
|
|
121
|
+
|
|
122
|
+
class BrokenStream:
|
|
123
|
+
async def aclose(self) -> None:
|
|
124
|
+
raise anyio.BrokenResourceError
|
|
125
|
+
|
|
126
|
+
transport._stream = BrokenStream() # type: ignore[assignment] # noqa: SLF001 - no public seam for a broken stream
|
|
127
|
+
|
|
128
|
+
await transport.close()
|
|
129
|
+
|
|
130
|
+
assert transport.is_connected is False
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@pytest.mark.usefixtures("echoing_trader")
|
|
134
|
+
async def test_reconnection_survives_a_transport_that_cannot_be_closed(
|
|
135
|
+
make_graph: Callable[..., ClientGraph],
|
|
136
|
+
connected: Callable[[CTraderClient], Awaitable[CTraderClient]],
|
|
137
|
+
server: FakeServer,
|
|
138
|
+
) -> None:
|
|
139
|
+
"""Failing to tear down the dead link must not abandon the reconnection.
|
|
140
|
+
|
|
141
|
+
Regression guard: closing the old transport is the first thing a
|
|
142
|
+
reconnection does, and on an already-broken socket that close can raise.
|
|
143
|
+
The exception used to be indistinguishable from an exhausted retry loop, so
|
|
144
|
+
the client stopped reconnecting permanently — while never having attempted
|
|
145
|
+
a single reconnection on that cycle.
|
|
146
|
+
"""
|
|
147
|
+
graph = make_graph()
|
|
148
|
+
close_cleanly = graph.transport.close
|
|
149
|
+
refusals = [anyio.BrokenResourceError()]
|
|
150
|
+
|
|
151
|
+
async def close_raising_once() -> None:
|
|
152
|
+
# Raise *after* the real teardown, the way the broken socket did: the
|
|
153
|
+
# stream is released first, and only the graceful TLS shutdown fails.
|
|
154
|
+
await close_cleanly()
|
|
155
|
+
if refusals:
|
|
156
|
+
raise refusals.pop()
|
|
157
|
+
|
|
158
|
+
graph.transport.close = close_raising_once # type: ignore[method-assign]
|
|
159
|
+
client = await connected(CTraderClient.from_graph(graph))
|
|
160
|
+
|
|
161
|
+
await server.drop_connection()
|
|
162
|
+
await server.wait_for_connections(2)
|
|
163
|
+
|
|
164
|
+
response = await client.protocol.request(ProtoOATraderReq(ctid_trader_account_id=10), ProtoOATraderRes)
|
|
165
|
+
|
|
166
|
+
assert response.ctid_trader_account_id == 10
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@pytest.mark.usefixtures("echoing_trader")
|
|
170
|
+
async def test_a_drop_reported_while_reconnecting_is_not_lost(
|
|
171
|
+
make_graph: Callable[..., ClientGraph],
|
|
172
|
+
connected: Callable[[CTraderClient], Awaitable[CTraderClient]],
|
|
173
|
+
server: FakeServer,
|
|
174
|
+
) -> None:
|
|
175
|
+
"""A drop reported mid-reconnection is acted on, not silently swallowed.
|
|
176
|
+
|
|
177
|
+
This is the only window in which nothing else can pick the report up: a
|
|
178
|
+
detector is refused while a reconnection is in flight, and the reader loop
|
|
179
|
+
exits as soon as it has reported, so the reconnection already running has to
|
|
180
|
+
go round again on its behalf. The report is made from a listener, which runs
|
|
181
|
+
inside that window by construction, and the link is left healthy so that no
|
|
182
|
+
*other* detector can mask the loss by noticing independently.
|
|
183
|
+
"""
|
|
184
|
+
graph = make_graph()
|
|
185
|
+
|
|
186
|
+
class ReportsADropWhileTheReconnectionIsInFlight:
|
|
187
|
+
def __init__(self) -> None:
|
|
188
|
+
self.reported = False
|
|
189
|
+
|
|
190
|
+
async def on_connection_lost(self) -> None: ...
|
|
191
|
+
|
|
192
|
+
async def on_connection_restored(self) -> None:
|
|
193
|
+
if self.reported:
|
|
194
|
+
return
|
|
195
|
+
self.reported = True
|
|
196
|
+
await graph.protocol.handle_disconnect()
|
|
197
|
+
|
|
198
|
+
graph.supervisor.add_listener(ReportsADropWhileTheReconnectionIsInFlight())
|
|
199
|
+
client = await connected(CTraderClient.from_graph(graph))
|
|
200
|
+
|
|
201
|
+
await server.drop_connection()
|
|
202
|
+
await server.wait_for_connections(3)
|
|
203
|
+
|
|
204
|
+
response = await client.protocol.request(ProtoOATraderReq(ctid_trader_account_id=12), ProtoOATraderRes)
|
|
205
|
+
|
|
206
|
+
assert response.ctid_trader_account_id == 12
|
|
207
|
+
|
|
208
|
+
|
|
112
209
|
@pytest.mark.usefixtures("echoing_trader")
|
|
113
210
|
async def test_a_failing_listener_does_not_block_reconnection(
|
|
114
211
|
make_graph: Callable[..., ClientGraph],
|
|
@@ -16,7 +16,7 @@ from ctrader_api_client._internal.proto import (
|
|
|
16
16
|
ProtoOARefreshTokenReq,
|
|
17
17
|
ProtoOATraderReq,
|
|
18
18
|
)
|
|
19
|
-
from ctrader_api_client.auth import AccountCredentials
|
|
19
|
+
from ctrader_api_client.auth import AccountCredentials, ReauthPolicy
|
|
20
20
|
from ctrader_api_client.events import (
|
|
21
21
|
AccountDisconnectEvent,
|
|
22
22
|
ReadyEvent,
|
|
@@ -35,6 +35,12 @@ from ..harness import FakeServer, ManualClock, Recorder, factories
|
|
|
35
35
|
|
|
36
36
|
ALMOST_EXPIRED = 10.0
|
|
37
37
|
CHECK_INTERVAL = 60.0
|
|
38
|
+
RETRY_WAIT = 1.0
|
|
39
|
+
|
|
40
|
+
# The heartbeat loop and the token-refresh loop, both parked on the clock once a
|
|
41
|
+
# client is up. Captured rather than assumed where a test needs to tell a new
|
|
42
|
+
# sleeper apart from these.
|
|
43
|
+
SLEEPERS_WHEN_CONNECTED = 2
|
|
38
44
|
|
|
39
45
|
|
|
40
46
|
async def authenticate(client: CTraderClient, server: FakeServer) -> AccountCredentials:
|
|
@@ -51,6 +57,20 @@ def rejecting(_request: betterproto.Message) -> betterproto.Message:
|
|
|
51
57
|
return factories.error_res(error_code="ACCOUNT_NOT_AUTHORIZED")
|
|
52
58
|
|
|
53
59
|
|
|
60
|
+
def cannot_route_once() -> Callable[[betterproto.Message], betterproto.Message]:
|
|
61
|
+
"""Refuse the first request the way a gateway refuses to route one, then relent.
|
|
62
|
+
|
|
63
|
+
`CANT_ROUTE_REQUEST` is transient and server-side: the same request a moment
|
|
64
|
+
later is routed normally.
|
|
65
|
+
"""
|
|
66
|
+
refusals = [factories.error_res(error_code="CANT_ROUTE_REQUEST", description="Cannot route request")]
|
|
67
|
+
|
|
68
|
+
def respond(_request: betterproto.Message) -> betterproto.Message:
|
|
69
|
+
return refusals.pop() if refusals else factories.app_auth_res()
|
|
70
|
+
|
|
71
|
+
return respond
|
|
72
|
+
|
|
73
|
+
|
|
54
74
|
def already_logged_in(_request: betterproto.Message) -> betterproto.Message:
|
|
55
75
|
"""Refuse the way the server refuses an account whose session is still live."""
|
|
56
76
|
return factories.error_res(
|
|
@@ -352,6 +372,63 @@ async def test_a_reconnect_that_cannot_re_authenticate_the_app_says_so(
|
|
|
352
372
|
assert reconnects.only.failed_accounts == ()
|
|
353
373
|
|
|
354
374
|
|
|
375
|
+
async def test_a_reconnect_keeps_retrying_application_authentication(
|
|
376
|
+
make_client: Callable[..., CTraderClient],
|
|
377
|
+
connected: Callable[[CTraderClient], Awaitable[CTraderClient]],
|
|
378
|
+
server: FakeServer,
|
|
379
|
+
) -> None:
|
|
380
|
+
"""A transient refusal must not cost the client the rest of its session.
|
|
381
|
+
|
|
382
|
+
Regression guard: application re-authentication used to be attempted exactly
|
|
383
|
+
once per reconnect. A single transient refusal left the link up but
|
|
384
|
+
unauthenticated, with no path back — every later request timed out, and
|
|
385
|
+
because the link itself was healthy nothing ever reconnected again.
|
|
386
|
+
"""
|
|
387
|
+
client = await connected(make_client(reauth_policy=ReauthPolicy(min_wait=0.0, max_wait=0.0)))
|
|
388
|
+
await authenticate(client, server)
|
|
389
|
+
ready: Recorder[ReadyEvent] = Recorder()
|
|
390
|
+
client.register_handler(ReadyEvent, ready)
|
|
391
|
+
server.on(ProtoOAApplicationAuthReq, cannot_route_once())
|
|
392
|
+
|
|
393
|
+
await server.drop_connection()
|
|
394
|
+
await ready.wait_for(1)
|
|
395
|
+
|
|
396
|
+
assert client.is_account_authorized(factories.ACCOUNT_ID) is True
|
|
397
|
+
assert ready.only.account_id == factories.ACCOUNT_ID
|
|
398
|
+
assert ready.only.is_reconnect is True
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
async def test_retrying_application_authentication_waits_out_its_backoff(
|
|
402
|
+
make_client: Callable[..., CTraderClient],
|
|
403
|
+
connected: Callable[[CTraderClient], Awaitable[CTraderClient]],
|
|
404
|
+
server: FakeServer,
|
|
405
|
+
clock: ManualClock,
|
|
406
|
+
) -> None:
|
|
407
|
+
"""Retrying is paced by the policy, not spun as fast as the server refuses.
|
|
408
|
+
|
|
409
|
+
A server that keeps refusing is usually one under strain; hammering it as
|
|
410
|
+
fast as the loop can go would pin a core and make that worse.
|
|
411
|
+
"""
|
|
412
|
+
client = await connected(make_client(reauth_policy=ReauthPolicy(min_wait=RETRY_WAIT, max_wait=RETRY_WAIT)))
|
|
413
|
+
await authenticate(client, server)
|
|
414
|
+
await clock.wait_for_sleepers(SLEEPERS_WHEN_CONNECTED)
|
|
415
|
+
parked = clock.sleeper_count
|
|
416
|
+
server.on(ProtoOAApplicationAuthReq, rejecting)
|
|
417
|
+
|
|
418
|
+
await server.drop_connection()
|
|
419
|
+
# The attempt the reconnection itself makes, which the server refuses.
|
|
420
|
+
await server.wait_for_request(ProtoOAApplicationAuthReq, 2)
|
|
421
|
+
# Recovery has taken it over and is parked on the backoff before retrying.
|
|
422
|
+
await clock.wait_for_sleepers(parked + 1)
|
|
423
|
+
|
|
424
|
+
assert len(server.requests_of(ProtoOAApplicationAuthReq)) == 2
|
|
425
|
+
|
|
426
|
+
await clock.advance(RETRY_WAIT)
|
|
427
|
+
await server.wait_for_request(ProtoOAApplicationAuthReq, 3)
|
|
428
|
+
|
|
429
|
+
assert len(server.requests_of(ProtoOAApplicationAuthReq)) == 3
|
|
430
|
+
|
|
431
|
+
|
|
355
432
|
async def test_an_account_a_reconnect_could_not_restore_is_not_authorized(
|
|
356
433
|
client: CTraderClient,
|
|
357
434
|
server: FakeServer,
|
|
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
|
|
File without changes
|
{ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/protos/vendor/OpenApiCommonMessages.proto
RENAMED
|
File without changes
|
{ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/protos/vendor/OpenApiCommonModelMessages.proto
RENAMED
|
File without changes
|
|
File without changes
|
{ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/protos/vendor/OpenApiModelMessages.proto
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/_internal/__init__.py
RENAMED
|
File without changes
|
{ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/_internal/clock.py
RENAMED
|
File without changes
|
|
File without changes
|
{ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/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.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/api/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/api/accounts.py
RENAMED
|
File without changes
|
{ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/api/market_data.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/auth/__init__.py
RENAMED
|
File without changes
|
{ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/auth/_refresh.py
RENAMED
|
File without changes
|
{ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/auth/_session.py
RENAMED
|
File without changes
|
{ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/auth/credentials.py
RENAMED
|
File without changes
|
{ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/auth/manager.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.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/connection/__init__.py
RENAMED
|
File without changes
|
{ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/connection/heartbeat.py
RENAMED
|
File without changes
|
{ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/connection/listener.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/events/__init__.py
RENAMED
|
File without changes
|
{ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/events/_execution.py
RENAMED
|
File without changes
|
{ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/events/emitter.py
RENAMED
|
File without changes
|
{ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/events/publisher.py
RENAMED
|
File without changes
|
{ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/events/router.py
RENAMED
|
File without changes
|
|
File without changes
|
{ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/models/__init__.py
RENAMED
|
File without changes
|
{ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/models/_base.py
RENAMED
|
File without changes
|
{ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/models/account.py
RENAMED
|
File without changes
|
|
File without changes
|
{ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/models/market_data.py
RENAMED
|
File without changes
|
{ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/models/order.py
RENAMED
|
File without changes
|
{ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/models/position.py
RENAMED
|
File without changes
|
{ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/src/ctrader_api_client/models/requests.py
RENAMED
|
File without changes
|
{ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/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.2 → ctrader_api_client-0.7.3}/tests/behavior/api/test_market_data.py
RENAMED
|
File without changes
|
{ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/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.2 → ctrader_api_client-0.7.3}/tests/behavior/auth/test_authentication.py
RENAMED
|
File without changes
|
{ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/tests/behavior/auth/test_token_lifecycle.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/tests/behavior/connection/test_framing.py
RENAMED
|
File without changes
|
{ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/tests/behavior/connection/test_heartbeat.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/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
|
|
File without changes
|
{ctrader_api_client-0.7.2 → ctrader_api_client-0.7.3}/tests/unit/test_market_data_conversion.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|