ctrader-api-client 0.7.3__tar.gz → 0.8.0__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.
Potentially problematic release.
This version of ctrader-api-client might be problematic. Click here for more details.
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/PKG-INFO +7 -3
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/README.md +6 -2
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/docs/api/exceptions.md +26 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/docs/getting-started.md +11 -2
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/pyproject.toml +1 -1
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/__init__.py +2 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/composition.py +1 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/config.py +8 -2
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/connection/protocol.py +61 -15
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/connection/transport.py +30 -12
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/exceptions.py +33 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/tests/behavior/connection/test_reconnection.py +219 -1
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/uv.lock +1 -1
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/.claude/settings.local.json +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/.github/workflows/docs.yml +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/.gitignore +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/.pre-commit-config.yaml +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/.python-version +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/Justfile +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/LICENSE +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/docs/api/accounts.md +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/docs/api/client.md +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/docs/api/enums.md +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/docs/api/events.md +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/docs/api/market-data.md +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/docs/api/models.md +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/docs/api/symbols.md +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/docs/api/trading.md +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/docs/index.md +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/mkdocs.yml +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/protos/SOURCE +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/protos/VERSION +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/protos/update.sh +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/protos/vendor/OpenApiCommonMessages.proto +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/protos/vendor/OpenApiCommonModelMessages.proto +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/protos/vendor/OpenApiMessages.proto +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/protos/vendor/OpenApiModelMessages.proto +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/scripts/fix_proto_imports.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/_internal/__init__.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/_internal/clock.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/_internal/conversions.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/_internal/messages.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/_internal/proto/OpenApiCommonMessages.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/_internal/proto/OpenApiCommonModelMessages.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/_internal/proto/OpenApiMessages.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/_internal/proto/OpenApiModelMessages.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/_internal/proto/__init__.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/_internal/serialization.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/api/__init__.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/api/_base.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/api/accounts.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/api/market_data.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/api/symbols.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/api/trading.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/auth/__init__.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/auth/_recovery.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/auth/_refresh.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/auth/_session.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/auth/credentials.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/auth/manager.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/auth/policy.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/auth/store.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/client.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/connection/__init__.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/connection/heartbeat.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/connection/listener.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/connection/supervisor.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/enums.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/events/__init__.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/events/_execution.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/events/emitter.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/events/publisher.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/events/router.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/events/types.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/models/__init__.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/models/_base.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/models/account.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/models/deal.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/models/market_data.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/models/order.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/models/position.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/models/requests.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/models/symbol.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/py.typed +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/tests/__init__.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/tests/behavior/__init__.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/tests/behavior/api/__init__.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/tests/behavior/api/conftest.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/tests/behavior/api/test_market_data.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/tests/behavior/api/test_reference_data.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/tests/behavior/api/test_subscription_restore.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/tests/behavior/api/test_trading.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/tests/behavior/auth/__init__.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/tests/behavior/auth/conftest.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/tests/behavior/auth/test_authentication.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/tests/behavior/auth/test_token_lifecycle.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/tests/behavior/connection/__init__.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/tests/behavior/connection/conftest.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/tests/behavior/connection/test_event_dispatch.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/tests/behavior/connection/test_framing.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/tests/behavior/connection/test_heartbeat.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/tests/behavior/connection/test_request_response.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/tests/behavior/events/__init__.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/tests/behavior/events/conftest.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/tests/behavior/events/test_routing.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/tests/behavior/events/test_subscriptions.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/tests/behavior/test_client.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/tests/conftest.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/tests/harness/__init__.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/tests/harness/clock.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/tests/harness/factories.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/tests/harness/recorder.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/tests/harness/server.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/tests/harness/signals.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/tests/harness/stub_protocol.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/tests/harness/wire.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/tests/unit/__init__.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/tests/unit/test_api_errors.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/tests/unit/test_market_data_conversion.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/tests/unit/test_request_conversion.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/tests/unit/test_symbol_conversion.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/tests/unit/test_trading_records.py +0 -0
- {ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/tests/unit/test_wire_format.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ctrader-api-client
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: API Client to interact with the cTrader Open API spec
|
|
5
5
|
Author-email: Elio <elioachukri@pm.me>
|
|
6
6
|
License-File: LICENSE
|
|
@@ -354,10 +354,14 @@ config = ClientConfig(
|
|
|
354
354
|
heartbeat_timeout=30.0, # Or 0 to disable server heartbeat checks
|
|
355
355
|
request_timeout=30.0,
|
|
356
356
|
|
|
357
|
-
# Reconnection
|
|
358
|
-
|
|
357
|
+
# Reconnection: None retries for as long as the client is open (the
|
|
358
|
+
# default), an integer caps the attempts, 0 disables reconnection. A
|
|
359
|
+
# finite budget that runs out raises CTraderReconnectAbandonedError out
|
|
360
|
+
# of the `async with client:` block.
|
|
361
|
+
reconnect_attempts=None,
|
|
359
362
|
reconnect_min_wait=1.0,
|
|
360
363
|
reconnect_max_wait=60.0,
|
|
364
|
+
connect_timeout=30.0,
|
|
361
365
|
|
|
362
366
|
# Token refresh: when to refresh access tokens and how hard to retry
|
|
363
367
|
refresh_policy=RefreshPolicy(
|
|
@@ -335,10 +335,14 @@ config = ClientConfig(
|
|
|
335
335
|
heartbeat_timeout=30.0, # Or 0 to disable server heartbeat checks
|
|
336
336
|
request_timeout=30.0,
|
|
337
337
|
|
|
338
|
-
# Reconnection
|
|
339
|
-
|
|
338
|
+
# Reconnection: None retries for as long as the client is open (the
|
|
339
|
+
# default), an integer caps the attempts, 0 disables reconnection. A
|
|
340
|
+
# finite budget that runs out raises CTraderReconnectAbandonedError out
|
|
341
|
+
# of the `async with client:` block.
|
|
342
|
+
reconnect_attempts=None,
|
|
340
343
|
reconnect_min_wait=1.0,
|
|
341
344
|
reconnect_max_wait=60.0,
|
|
345
|
+
connect_timeout=30.0,
|
|
342
346
|
|
|
343
347
|
# Token refresh: when to refresh access tokens and how hard to retry
|
|
344
348
|
refresh_policy=RefreshPolicy(
|
|
@@ -153,6 +153,32 @@ other handlers for that event.
|
|
|
153
153
|
options:
|
|
154
154
|
show_source: false
|
|
155
155
|
|
|
156
|
+
::: ctrader_api_client.CTraderReconnectAbandonedError
|
|
157
|
+
options:
|
|
158
|
+
show_source: false
|
|
159
|
+
|
|
160
|
+
This is the one connection exception that reaches you without your having called
|
|
161
|
+
anything: it is raised out of the `async with client:` block itself. Reconnection
|
|
162
|
+
is unbounded by default, so seeing it means either that `reconnect_attempts` was
|
|
163
|
+
set to a finite number and every one was spent, or that something failed which
|
|
164
|
+
the reconnection logic did not anticipate.
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
while True:
|
|
168
|
+
try:
|
|
169
|
+
async with client:
|
|
170
|
+
await asyncio.Event().wait()
|
|
171
|
+
except CTraderReconnectAbandonedError:
|
|
172
|
+
logger.exception("Client abandoned its connection; starting a new one")
|
|
173
|
+
await asyncio.sleep(30)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Catching it is only worthwhile if you intend to build a fresh client, since the
|
|
177
|
+
one that raised it is finished. Letting it propagate and having a process
|
|
178
|
+
supervisor restart you is an equally good answer, and the reason it is raised
|
|
179
|
+
rather than logged: a client that has stopped reconnecting has no reader and no
|
|
180
|
+
heartbeat left to notice anything, so nothing short of a restart brings it back.
|
|
181
|
+
|
|
156
182
|
### Authentication
|
|
157
183
|
|
|
158
184
|
::: ctrader_api_client.AuthenticationError
|
|
@@ -312,13 +312,22 @@ config = ClientConfig(
|
|
|
312
312
|
heartbeat_timeout=0, # 0 to disable server heartbeat checks (default)
|
|
313
313
|
request_timeout=30.0,
|
|
314
314
|
|
|
315
|
-
# Reconnection
|
|
316
|
-
|
|
315
|
+
# Reconnection: None retries for as long as the client is open (the
|
|
316
|
+
# default), an integer caps the attempts, 0 disables reconnection.
|
|
317
|
+
reconnect_attempts=None,
|
|
317
318
|
reconnect_min_wait=1.0,
|
|
318
319
|
reconnect_max_wait=60.0,
|
|
320
|
+
connect_timeout=30.0,
|
|
319
321
|
)
|
|
320
322
|
```
|
|
321
323
|
|
|
324
|
+
Reconnection is unbounded by default because a finite budget that runs out
|
|
325
|
+
leaves a client that can never recover: there is no reader and no heartbeat left
|
|
326
|
+
to notice anything, so it stays offline until the process restarts. If you do
|
|
327
|
+
set a finite `reconnect_attempts`, spending it raises
|
|
328
|
+
`CTraderReconnectAbandonedError` out of the `async with client:` block rather
|
|
329
|
+
than leaving the client alive and permanently disconnected.
|
|
330
|
+
|
|
322
331
|
## Next Steps
|
|
323
332
|
|
|
324
333
|
- [API Reference - Client](api/client.md) - Full client documentation
|
|
@@ -92,6 +92,7 @@ from .exceptions import (
|
|
|
92
92
|
CTraderConnectionFailedError,
|
|
93
93
|
CTraderConnectionTimeoutError,
|
|
94
94
|
CTraderError,
|
|
95
|
+
CTraderReconnectAbandonedError,
|
|
95
96
|
DeserializationError,
|
|
96
97
|
FramingError,
|
|
97
98
|
ProtocolError,
|
|
@@ -139,6 +140,7 @@ __all__ = [
|
|
|
139
140
|
"CTraderConnectionFailedError",
|
|
140
141
|
"CTraderConnectionTimeoutError",
|
|
141
142
|
"CTraderError",
|
|
143
|
+
"CTraderReconnectAbandonedError",
|
|
142
144
|
"ClientConfig",
|
|
143
145
|
"ClientDisconnectEvent",
|
|
144
146
|
"CloseDetail",
|
|
@@ -17,7 +17,12 @@ class ClientConfig(BaseModel):
|
|
|
17
17
|
heartbeat_interval: Seconds between heartbeat sends.
|
|
18
18
|
heartbeat_timeout: Seconds without server-initiated messages before disconnect. Set to 0 to disable.
|
|
19
19
|
request_timeout: Default timeout for API requests in seconds.
|
|
20
|
-
|
|
20
|
+
connect_timeout: Seconds allowed for the TCP connection and TLS
|
|
21
|
+
handshake together, after which the attempt is failed and retried.
|
|
22
|
+
reconnect_attempts: Max reconnection attempts. None (the default)
|
|
23
|
+
retries for as long as the client is open. 0 disables reconnection.
|
|
24
|
+
A finite budget that runs out abandons the connection and raises
|
|
25
|
+
CTraderReconnectAbandonedError out of the `async with` block.
|
|
21
26
|
reconnect_min_wait: Initial wait between reconnection attempts.
|
|
22
27
|
reconnect_max_wait: Maximum wait between reconnection attempts.
|
|
23
28
|
refresh_policy: When to refresh access tokens and how hard to try.
|
|
@@ -51,9 +56,10 @@ class ClientConfig(BaseModel):
|
|
|
51
56
|
|
|
52
57
|
# Request settings
|
|
53
58
|
request_timeout: float = Field(default=30.0, gt=0)
|
|
59
|
+
connect_timeout: float = Field(default=30.0, gt=0)
|
|
54
60
|
|
|
55
61
|
# Reconnection settings
|
|
56
|
-
reconnect_attempts: int = Field(default=
|
|
62
|
+
reconnect_attempts: int | None = Field(default=None, ge=0)
|
|
57
63
|
reconnect_min_wait: float = Field(default=1.0, gt=0)
|
|
58
64
|
reconnect_max_wait: float = Field(default=60.0, gt=0)
|
|
59
65
|
|
{ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/connection/protocol.py
RENAMED
|
@@ -12,6 +12,7 @@ from tenacity import (
|
|
|
12
12
|
AsyncRetrying,
|
|
13
13
|
retry_if_exception_type,
|
|
14
14
|
stop_after_attempt,
|
|
15
|
+
stop_never,
|
|
15
16
|
wait_exponential,
|
|
16
17
|
)
|
|
17
18
|
|
|
@@ -29,6 +30,7 @@ from ..exceptions import (
|
|
|
29
30
|
CTraderConnectionClosedError,
|
|
30
31
|
CTraderConnectionFailedError,
|
|
31
32
|
CTraderConnectionTimeoutError,
|
|
33
|
+
CTraderReconnectAbandonedError,
|
|
32
34
|
FramingError,
|
|
33
35
|
)
|
|
34
36
|
from .listener import ConnectionListener
|
|
@@ -73,7 +75,7 @@ class Protocol:
|
|
|
73
75
|
def __init__(
|
|
74
76
|
self,
|
|
75
77
|
transport: Transport,
|
|
76
|
-
reconnect_attempts: int =
|
|
78
|
+
reconnect_attempts: int | None = None,
|
|
77
79
|
reconnect_min_wait: float = 1.0,
|
|
78
80
|
reconnect_max_wait: float = 60.0,
|
|
79
81
|
) -> None:
|
|
@@ -81,7 +83,10 @@ class Protocol:
|
|
|
81
83
|
|
|
82
84
|
Args:
|
|
83
85
|
transport: The underlying transport for sending/receiving data.
|
|
84
|
-
reconnect_attempts: Maximum reconnection attempts
|
|
86
|
+
reconnect_attempts: Maximum reconnection attempts. None retries for
|
|
87
|
+
as long as the client is open, which is the default: an outage
|
|
88
|
+
that outlasts a finite budget otherwise leaves a client that
|
|
89
|
+
can never recover. 0 disables reconnection entirely.
|
|
85
90
|
reconnect_min_wait: Initial wait between attempts (seconds).
|
|
86
91
|
reconnect_max_wait: Maximum wait between attempts (seconds).
|
|
87
92
|
"""
|
|
@@ -500,6 +505,11 @@ class Protocol:
|
|
|
500
505
|
working — reported through `handle_disconnect` as `_redo_requested`, the
|
|
501
506
|
only route left once the reader has exited — is reconnected too instead
|
|
502
507
|
of being stranded.
|
|
508
|
+
|
|
509
|
+
Raises:
|
|
510
|
+
CTraderReconnectAbandonedError: If reconnection is given up on. This
|
|
511
|
+
is deliberately allowed to escape into the protocol task group
|
|
512
|
+
and tear the client down: see :meth:`_abandon`.
|
|
503
513
|
"""
|
|
504
514
|
try:
|
|
505
515
|
while True:
|
|
@@ -535,18 +545,46 @@ class Protocol:
|
|
|
535
545
|
await self._notify_connection_lost()
|
|
536
546
|
except (CTraderConnectionFailedError, CTraderConnectionClosedError) as e:
|
|
537
547
|
logger.error("Reconnection failed, giving up: %s", e)
|
|
538
|
-
self.
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
#
|
|
548
|
+
self._abandon("connection lost and reconnection failed", e)
|
|
549
|
+
except Exception as e:
|
|
550
|
+
# Nothing here is expected to fail this way — the retry loop below
|
|
551
|
+
# absorbs every failure connecting — so treat it as a defect rather
|
|
552
|
+
# than a state to sit quietly in.
|
|
543
553
|
logger.exception("Unexpected error while reconnecting")
|
|
544
|
-
self.
|
|
545
|
-
self._fail_pending("Reconnection error")
|
|
554
|
+
self._abandon("unexpected error while reconnecting", e)
|
|
546
555
|
finally:
|
|
547
556
|
self._reconnecting = False
|
|
548
557
|
self._redo_requested = False
|
|
549
558
|
|
|
559
|
+
def _abandon(self, reason: str, cause: BaseException) -> None:
|
|
560
|
+
"""Stop the protocol and make giving up impossible to miss.
|
|
561
|
+
|
|
562
|
+
A client that has stopped reconnecting cannot recover on its own: the
|
|
563
|
+
reader has exited, the heartbeat loop returns as soon as its next write
|
|
564
|
+
fails, and `handle_disconnect` refuses everything once `_running` is
|
|
565
|
+
False. Nothing is left that could try again. Staying alive in that state
|
|
566
|
+
only means a consumer polling `is_connected` sees a link that is down
|
|
567
|
+
and assumes, reasonably and wrongly, that something is working on it.
|
|
568
|
+
|
|
569
|
+
So the failure is raised instead of recorded. It escapes into the
|
|
570
|
+
protocol task group, through the supervisor's, and out of the
|
|
571
|
+
`async with client:` block, where a process supervisor will see it.
|
|
572
|
+
|
|
573
|
+
Raises:
|
|
574
|
+
CTraderReconnectAbandonedError: Always, unless reconnection was
|
|
575
|
+
disabled by configuration, which is a choice rather than a
|
|
576
|
+
failure and keeps the previous behaviour of a client that
|
|
577
|
+
rejects further requests.
|
|
578
|
+
"""
|
|
579
|
+
self._running = False
|
|
580
|
+
# Wake all pending requests so callers fail fast instead of hanging.
|
|
581
|
+
self._fail_pending(reason)
|
|
582
|
+
|
|
583
|
+
if self._reconnect_attempts == 0:
|
|
584
|
+
return
|
|
585
|
+
|
|
586
|
+
raise CTraderReconnectAbandonedError(reason, cause) from cause
|
|
587
|
+
|
|
550
588
|
def _fail_pending(self, reason: str) -> None:
|
|
551
589
|
"""Resolve every in-flight request with a connection error."""
|
|
552
590
|
for pending in self._pending.values():
|
|
@@ -556,27 +594,35 @@ class Protocol:
|
|
|
556
594
|
async def _reconnect(self) -> None:
|
|
557
595
|
"""Attempt reconnection with exponential backoff.
|
|
558
596
|
|
|
597
|
+
Retries on *any* failure rather than on an enumerated set of exception
|
|
598
|
+
types. The budget exists to outlast a server that cannot be reached,
|
|
599
|
+
and an unrecognised exception is no evidence that it can be: a
|
|
600
|
+
whitelist here meant an exception nobody had thought of skipped the
|
|
601
|
+
retries entirely and went straight to being fatal.
|
|
602
|
+
|
|
559
603
|
Raises:
|
|
560
|
-
CTraderConnectionClosedError: If reconnection is disabled
|
|
561
|
-
CTraderConnectionFailedError: If
|
|
604
|
+
CTraderConnectionClosedError: If reconnection is disabled.
|
|
605
|
+
CTraderConnectionFailedError: If every attempt in the budget failed.
|
|
562
606
|
"""
|
|
563
607
|
if self._reconnect_attempts == 0:
|
|
564
608
|
raise CTraderConnectionClosedError("Connection lost and reconnection disabled")
|
|
565
609
|
|
|
610
|
+
budget = "unlimited" if self._reconnect_attempts is None else self._reconnect_attempts
|
|
611
|
+
|
|
566
612
|
async for attempt in AsyncRetrying(
|
|
567
|
-
stop=stop_after_attempt(self._reconnect_attempts),
|
|
613
|
+
stop=stop_never if self._reconnect_attempts is None else stop_after_attempt(self._reconnect_attempts),
|
|
568
614
|
wait=wait_exponential(
|
|
569
615
|
min=self._reconnect_min_wait,
|
|
570
616
|
max=self._reconnect_max_wait,
|
|
571
617
|
),
|
|
572
|
-
retry=retry_if_exception_type(
|
|
618
|
+
retry=retry_if_exception_type(Exception),
|
|
573
619
|
reraise=True,
|
|
574
620
|
):
|
|
575
621
|
with attempt:
|
|
576
622
|
logger.debug(
|
|
577
|
-
"Reconnection attempt %d/%
|
|
623
|
+
"Reconnection attempt %d/%s",
|
|
578
624
|
attempt.retry_state.attempt_number,
|
|
579
|
-
|
|
625
|
+
budget,
|
|
580
626
|
)
|
|
581
627
|
await self._transport.connect()
|
|
582
628
|
|
{ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/connection/transport.py
RENAMED
|
@@ -18,17 +18,21 @@ class Transport:
|
|
|
18
18
|
Handles raw socket connections without knowledge of protobuf or message semantics.
|
|
19
19
|
"""
|
|
20
20
|
|
|
21
|
-
def __init__(self, host: str, port: int, use_ssl: bool = True) -> None:
|
|
21
|
+
def __init__(self, host: str, port: int, use_ssl: bool = True, connect_timeout: float = 30.0) -> None:
|
|
22
22
|
"""Initialize transport configuration.
|
|
23
23
|
|
|
24
24
|
Args:
|
|
25
25
|
host: The server hostname to connect to.
|
|
26
26
|
port: The server port to connect to.
|
|
27
27
|
use_ssl: Whether to use SSL/TLS encryption. Defaults to True.
|
|
28
|
+
connect_timeout: Seconds to allow for the TCP connection and the
|
|
29
|
+
TLS handshake together. Bounds the one failure mode a caller
|
|
30
|
+
cannot retry: a handshake that neither completes nor fails.
|
|
28
31
|
"""
|
|
29
32
|
self._host = host
|
|
30
33
|
self._port = port
|
|
31
34
|
self._ssl = use_ssl
|
|
35
|
+
self._connect_timeout = connect_timeout
|
|
32
36
|
self._stream: ByteStream | None = None
|
|
33
37
|
|
|
34
38
|
@property
|
|
@@ -62,6 +66,12 @@ class Transport:
|
|
|
62
66
|
|
|
63
67
|
If already connected, this method returns immediately.
|
|
64
68
|
|
|
69
|
+
Either returns with a usable stream or raises
|
|
70
|
+
`CTraderConnectionFailedError`. There is deliberately no third outcome:
|
|
71
|
+
anything that goes wrong on the way to a connection *is* a failure to
|
|
72
|
+
connect, whatever its type, so callers can retry on one exception
|
|
73
|
+
rather than on a list of them that has to be kept complete.
|
|
74
|
+
|
|
65
75
|
Raises:
|
|
66
76
|
CTraderConnectionFailedError: If connection cannot be established.
|
|
67
77
|
"""
|
|
@@ -69,17 +79,25 @@ class Transport:
|
|
|
69
79
|
return # Already connected
|
|
70
80
|
|
|
71
81
|
try:
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
82
|
+
# Every failure is translated, not an enumerated set of them. A TLS
|
|
83
|
+
# handshake cut short by the peer surfaces as anyio's
|
|
84
|
+
# BrokenResourceError, which is not an OSError and so used to
|
|
85
|
+
# escape untranslated — past the retry loop that only knew
|
|
86
|
+
# CTraderConnectionFailedError, and into the handler that reads an
|
|
87
|
+
# unrecognised exception as fatal. Cancellation still passes
|
|
88
|
+
# through: anyio raises it from BaseException, not Exception.
|
|
89
|
+
with anyio.fail_after(self._connect_timeout):
|
|
90
|
+
if self._ssl:
|
|
91
|
+
ssl_context = ssl.create_default_context()
|
|
92
|
+
self._stream = await anyio.connect_tcp(
|
|
93
|
+
self._host,
|
|
94
|
+
self._port,
|
|
95
|
+
ssl_context=ssl_context,
|
|
96
|
+
tls_standard_compatible=True,
|
|
97
|
+
)
|
|
98
|
+
else:
|
|
99
|
+
self._stream = await anyio.connect_tcp(self._host, self._port)
|
|
100
|
+
except Exception as e:
|
|
83
101
|
raise CTraderConnectionFailedError(self._host, self._port, e) from e
|
|
84
102
|
|
|
85
103
|
if self._ssl:
|
|
@@ -68,6 +68,39 @@ class CTraderConnectionTimeoutError(CTraderConnectionError):
|
|
|
68
68
|
super().__init__(f"{operation} timed out after {timeout_seconds}s")
|
|
69
69
|
|
|
70
70
|
|
|
71
|
+
class CTraderReconnectAbandonedError(CTraderConnectionError):
|
|
72
|
+
"""The client tried to restore a dropped link and gave up.
|
|
73
|
+
|
|
74
|
+
Raised out of the `async with client:` block, tearing the client down
|
|
75
|
+
rather than leaving it alive but permanently offline. A client that has
|
|
76
|
+
stopped reconnecting can never recover on its own — there is no reader and
|
|
77
|
+
no heartbeat left to notice anything — so the only useful thing it can do
|
|
78
|
+
is say so where a supervisor will see it.
|
|
79
|
+
|
|
80
|
+
Reaching this is not expected. Reconnection is unbounded by default, so a
|
|
81
|
+
server that stays unreachable produces retries rather than this error. It
|
|
82
|
+
means either that `reconnect_attempts` was set to a finite number and every
|
|
83
|
+
one was used, or that something went wrong that the reconnection logic did
|
|
84
|
+
not anticipate.
|
|
85
|
+
|
|
86
|
+
Not raised when reconnection is disabled outright with
|
|
87
|
+
`reconnect_attempts=0`: not reconnecting is the documented outcome there,
|
|
88
|
+
not a failure.
|
|
89
|
+
|
|
90
|
+
Attributes:
|
|
91
|
+
reason: What was being attempted when it was abandoned.
|
|
92
|
+
cause: The final underlying failure, also set as `__cause__`.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
def __init__(self, reason: str, cause: BaseException | None = None) -> None:
|
|
96
|
+
self.reason = reason
|
|
97
|
+
self.cause = cause
|
|
98
|
+
message = f"Reconnection abandoned: {reason}"
|
|
99
|
+
if cause:
|
|
100
|
+
message += f" ({cause})"
|
|
101
|
+
super().__init__(message)
|
|
102
|
+
|
|
103
|
+
|
|
71
104
|
# =============================================================================
|
|
72
105
|
# Authentication Errors
|
|
73
106
|
# =============================================================================
|
{ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/tests/behavior/connection/test_reconnection.py
RENAMED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import logging
|
|
6
|
+
import ssl
|
|
6
7
|
from collections.abc import Awaitable, Callable
|
|
8
|
+
from typing import Any
|
|
7
9
|
|
|
8
10
|
import anyio
|
|
9
11
|
import pytest
|
|
@@ -15,12 +17,64 @@ from ctrader_api_client._internal.proto import (
|
|
|
15
17
|
ProtoOATraderRes,
|
|
16
18
|
)
|
|
17
19
|
from ctrader_api_client.composition import ClientGraph
|
|
20
|
+
from ctrader_api_client.config import ClientConfig
|
|
18
21
|
from ctrader_api_client.connection import Transport
|
|
19
|
-
from ctrader_api_client.exceptions import
|
|
22
|
+
from ctrader_api_client.exceptions import (
|
|
23
|
+
CTraderConnectionClosedError,
|
|
24
|
+
CTraderConnectionFailedError,
|
|
25
|
+
CTraderReconnectAbandonedError,
|
|
26
|
+
)
|
|
20
27
|
|
|
21
28
|
from ...harness import FakeServer, Recorder, factories
|
|
22
29
|
|
|
23
30
|
|
|
31
|
+
# Everything a connection attempt has been seen to fail with, plus one thing it
|
|
32
|
+
# has not. The point of the last entry is that this list can never be complete:
|
|
33
|
+
# a new anyio or CPython release is free to raise something nobody here thought
|
|
34
|
+
# of, and the behaviour under test has to hold for that too. Tests below are
|
|
35
|
+
# parametrised over this rather than written per exception type, because a
|
|
36
|
+
# guard written against one exception is exactly what let the second outage
|
|
37
|
+
# through after the first was fixed.
|
|
38
|
+
CONNECT_FAILURES: list[Exception] = [
|
|
39
|
+
# What a TLS handshake cut short by the peer actually raises. This is the
|
|
40
|
+
# one that took production down: not an OSError, so it escaped translation.
|
|
41
|
+
anyio.BrokenResourceError(),
|
|
42
|
+
anyio.EndOfStream(),
|
|
43
|
+
anyio.ClosedResourceError(),
|
|
44
|
+
ssl.SSLEOFError("EOF occurred in violation of protocol"),
|
|
45
|
+
ssl.SSLCertVerificationError("certificate verify failed"),
|
|
46
|
+
OSError("connection refused"),
|
|
47
|
+
TimeoutError("timed out"),
|
|
48
|
+
RuntimeError("a failure mode nobody has predicted yet"),
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def failing_connect(
|
|
53
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
54
|
+
failure: Exception,
|
|
55
|
+
times: int | None = 1,
|
|
56
|
+
) -> None:
|
|
57
|
+
"""Make the next `times` connection attempts fail with `failure`.
|
|
58
|
+
|
|
59
|
+
Patches anyio rather than `Transport.connect`, so that the transport's own
|
|
60
|
+
translation of the failure is part of what is under test. `times=None`
|
|
61
|
+
fails every attempt.
|
|
62
|
+
"""
|
|
63
|
+
connect_tcp = anyio.connect_tcp
|
|
64
|
+
remaining = times
|
|
65
|
+
|
|
66
|
+
async def connect(*args: Any, **kwargs: Any) -> Any:
|
|
67
|
+
nonlocal remaining
|
|
68
|
+
if remaining is None:
|
|
69
|
+
raise failure
|
|
70
|
+
if remaining > 0:
|
|
71
|
+
remaining -= 1
|
|
72
|
+
raise failure
|
|
73
|
+
return await connect_tcp(*args, **kwargs)
|
|
74
|
+
|
|
75
|
+
monkeypatch.setattr(anyio, "connect_tcp", connect)
|
|
76
|
+
|
|
77
|
+
|
|
24
78
|
@pytest.mark.usefixtures("echoing_trader")
|
|
25
79
|
async def test_client_reconnects_after_the_server_drops_the_link(
|
|
26
80
|
client: CTraderClient,
|
|
@@ -206,6 +260,170 @@ async def test_a_drop_reported_while_reconnecting_is_not_lost(
|
|
|
206
260
|
assert response.ctid_trader_account_id == 12
|
|
207
261
|
|
|
208
262
|
|
|
263
|
+
@pytest.mark.parametrize("failure", CONNECT_FAILURES, ids=lambda failure: type(failure).__name__)
|
|
264
|
+
async def test_connecting_reports_every_failure_as_a_connection_failure(
|
|
265
|
+
failure: Exception,
|
|
266
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
267
|
+
) -> None:
|
|
268
|
+
"""`connect()` has two outcomes, not three: a stream, or a failure to connect.
|
|
269
|
+
|
|
270
|
+
Regression guard: it used to translate only `OSError`, so anything else —
|
|
271
|
+
`BrokenResourceError` from a broken TLS handshake, in production — came out
|
|
272
|
+
as itself. Callers retry on the translated type, so an untranslated failure
|
|
273
|
+
skipped the retries and was read as fatal instead.
|
|
274
|
+
"""
|
|
275
|
+
failing_connect(monkeypatch, failure)
|
|
276
|
+
transport = Transport(host="127.0.0.1", port=1, use_ssl=False)
|
|
277
|
+
|
|
278
|
+
with pytest.raises(CTraderConnectionFailedError) as raised:
|
|
279
|
+
await transport.connect()
|
|
280
|
+
|
|
281
|
+
assert raised.value.cause is failure
|
|
282
|
+
assert transport.is_connected is False
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
@pytest.mark.usefixtures("echoing_trader")
|
|
286
|
+
@pytest.mark.parametrize("failure", CONNECT_FAILURES, ids=lambda failure: type(failure).__name__)
|
|
287
|
+
async def test_reconnection_survives_any_failure_to_connect(
|
|
288
|
+
failure: Exception,
|
|
289
|
+
make_client: Callable[..., CTraderClient],
|
|
290
|
+
connected: Callable[[CTraderClient], Awaitable[CTraderClient]],
|
|
291
|
+
server: FakeServer,
|
|
292
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
293
|
+
) -> None:
|
|
294
|
+
"""A failed connection attempt is a reason to try again, whatever it was.
|
|
295
|
+
|
|
296
|
+
Regression guard for the second production outage: a `BrokenResourceError`
|
|
297
|
+
from a TLS handshake the broker cut short went straight past a retry loop
|
|
298
|
+
that only recognised `CTraderConnectionFailedError`, and the client died on
|
|
299
|
+
attempt 1 of 5 without spending its budget. The exception type must not be
|
|
300
|
+
what decides whether the client survives, so this holds for a failure no
|
|
301
|
+
one has anticipated as much as for the one that happened.
|
|
302
|
+
"""
|
|
303
|
+
client = await connected(make_client())
|
|
304
|
+
failing_connect(monkeypatch, failure)
|
|
305
|
+
|
|
306
|
+
await server.drop_connection()
|
|
307
|
+
await server.wait_for_connections(2)
|
|
308
|
+
|
|
309
|
+
response = await client.protocol.request(ProtoOATraderReq(ctid_trader_account_id=20), ProtoOATraderRes)
|
|
310
|
+
|
|
311
|
+
assert response.ctid_trader_account_id == 20
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
@pytest.mark.usefixtures("echoing_trader")
|
|
315
|
+
async def test_reconnection_outlasts_an_outage_longer_than_the_old_budget(
|
|
316
|
+
make_client: Callable[..., CTraderClient],
|
|
317
|
+
connected: Callable[[CTraderClient], Awaitable[CTraderClient]],
|
|
318
|
+
server: FakeServer,
|
|
319
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
320
|
+
) -> None:
|
|
321
|
+
"""Reconnection keeps going by default rather than running out.
|
|
322
|
+
|
|
323
|
+
The default used to be five attempts, roughly fifteen seconds of backoff —
|
|
324
|
+
less than a router reboot, after which the client was offline for good.
|
|
325
|
+
Eight consecutive failures here is more than that budget would have
|
|
326
|
+
survived, so this fails if the default ever becomes finite again.
|
|
327
|
+
"""
|
|
328
|
+
client = await connected(make_client())
|
|
329
|
+
failing_connect(monkeypatch, anyio.BrokenResourceError(), times=8)
|
|
330
|
+
|
|
331
|
+
await server.drop_connection()
|
|
332
|
+
await server.wait_for_connections(2)
|
|
333
|
+
|
|
334
|
+
response = await client.protocol.request(ProtoOATraderReq(ctid_trader_account_id=21), ProtoOATraderRes)
|
|
335
|
+
|
|
336
|
+
assert response.ctid_trader_account_id == 21
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def test_reconnection_is_unbounded_unless_configured_otherwise() -> None:
|
|
340
|
+
"""The default is stated here so changing it has to be deliberate."""
|
|
341
|
+
config = ClientConfig(client_id="id", client_secret="secret")
|
|
342
|
+
|
|
343
|
+
assert config.reconnect_attempts is None
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
@pytest.mark.usefixtures("echoing_trader")
|
|
347
|
+
async def test_a_connection_attempt_that_hangs_is_given_up_on_and_retried(
|
|
348
|
+
make_client: Callable[..., CTraderClient],
|
|
349
|
+
connected: Callable[[CTraderClient], Awaitable[CTraderClient]],
|
|
350
|
+
server: FakeServer,
|
|
351
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
352
|
+
) -> None:
|
|
353
|
+
"""A handshake that never finishes is bounded, not waited on forever.
|
|
354
|
+
|
|
355
|
+
The failure with no exception at all: a TLS handshake that neither
|
|
356
|
+
completes nor fails leaves the reconnection parked on it, which looks
|
|
357
|
+
exactly like the permanent outage a raised exception used to cause and
|
|
358
|
+
leaves nothing in the log to say so.
|
|
359
|
+
"""
|
|
360
|
+
client = await connected(make_client(connect_timeout=0.05))
|
|
361
|
+
connect_tcp = anyio.connect_tcp
|
|
362
|
+
hung = False
|
|
363
|
+
|
|
364
|
+
async def connect(*args: Any, **kwargs: Any) -> Any:
|
|
365
|
+
nonlocal hung
|
|
366
|
+
if not hung:
|
|
367
|
+
hung = True
|
|
368
|
+
await anyio.sleep_forever()
|
|
369
|
+
return await connect_tcp(*args, **kwargs)
|
|
370
|
+
|
|
371
|
+
monkeypatch.setattr(anyio, "connect_tcp", connect)
|
|
372
|
+
|
|
373
|
+
await server.drop_connection()
|
|
374
|
+
await server.wait_for_connections(2)
|
|
375
|
+
|
|
376
|
+
response = await client.protocol.request(ProtoOATraderReq(ctid_trader_account_id=22), ProtoOATraderRes)
|
|
377
|
+
|
|
378
|
+
assert hung
|
|
379
|
+
assert response.ctid_trader_account_id == 22
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
async def test_abandoning_reconnection_raises_out_of_the_client_block(
|
|
383
|
+
make_client: Callable[..., CTraderClient],
|
|
384
|
+
server: FakeServer,
|
|
385
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
386
|
+
) -> None:
|
|
387
|
+
"""Giving up tears the client down instead of leaving it quietly offline.
|
|
388
|
+
|
|
389
|
+
A client that has stopped reconnecting cannot recover on its own, so
|
|
390
|
+
staying alive only means a consumer polling `is_connected` sees a link that
|
|
391
|
+
is down and assumes something is working on restoring it. In production
|
|
392
|
+
that assumption was logged once every few seconds for twenty-three minutes.
|
|
393
|
+
"""
|
|
394
|
+
client = make_client(reconnect_attempts=2)
|
|
395
|
+
|
|
396
|
+
with pytest.raises(CTraderReconnectAbandonedError) as raised:
|
|
397
|
+
async with client:
|
|
398
|
+
failing_connect(monkeypatch, anyio.BrokenResourceError(), times=None)
|
|
399
|
+
await server.drop_connection()
|
|
400
|
+
# Park until the abandonment cancels us; the deadline turns a
|
|
401
|
+
# regression into a failure rather than a hung suite.
|
|
402
|
+
with anyio.fail_after(10):
|
|
403
|
+
await anyio.sleep_forever()
|
|
404
|
+
|
|
405
|
+
assert isinstance(raised.value.cause, CTraderConnectionFailedError)
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
async def test_disabling_reconnection_does_not_tear_the_client_down(
|
|
409
|
+
make_client: Callable[..., CTraderClient],
|
|
410
|
+
connected: Callable[[CTraderClient], Awaitable[CTraderClient]],
|
|
411
|
+
server: FakeServer,
|
|
412
|
+
) -> None:
|
|
413
|
+
"""Not reconnecting on request is an outcome, not a failure to report.
|
|
414
|
+
|
|
415
|
+
The counterpart to the test above: abandonment is raised because the client
|
|
416
|
+
tried and could not, which is never true when it was told not to try.
|
|
417
|
+
"""
|
|
418
|
+
client = await connected(make_client(reconnect_attempts=0))
|
|
419
|
+
|
|
420
|
+
await server.drop_connection()
|
|
421
|
+
await server.wait_for_disconnect()
|
|
422
|
+
|
|
423
|
+
with pytest.raises(CTraderConnectionClosedError):
|
|
424
|
+
await client.protocol.send_request(ProtoOATraderReq(ctid_trader_account_id=23), timeout=1)
|
|
425
|
+
|
|
426
|
+
|
|
209
427
|
@pytest.mark.usefixtures("echoing_trader")
|
|
210
428
|
async def test_a_failing_listener_does_not_block_reconnection(
|
|
211
429
|
make_graph: Callable[..., ClientGraph],
|
|
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.3 → ctrader_api_client-0.8.0}/protos/vendor/OpenApiCommonMessages.proto
RENAMED
|
File without changes
|
{ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/protos/vendor/OpenApiCommonModelMessages.proto
RENAMED
|
File without changes
|
|
File without changes
|
{ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/protos/vendor/OpenApiModelMessages.proto
RENAMED
|
File without changes
|
|
File without changes
|
{ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/_internal/__init__.py
RENAMED
|
File without changes
|
{ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/_internal/clock.py
RENAMED
|
File without changes
|
|
File without changes
|
{ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/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.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/api/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/api/accounts.py
RENAMED
|
File without changes
|
{ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/api/market_data.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/auth/__init__.py
RENAMED
|
File without changes
|
{ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/auth/_recovery.py
RENAMED
|
File without changes
|
{ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/auth/_refresh.py
RENAMED
|
File without changes
|
{ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/auth/_session.py
RENAMED
|
File without changes
|
{ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/auth/credentials.py
RENAMED
|
File without changes
|
{ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/auth/manager.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/connection/__init__.py
RENAMED
|
File without changes
|
{ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/connection/heartbeat.py
RENAMED
|
File without changes
|
{ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/connection/listener.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/events/__init__.py
RENAMED
|
File without changes
|
{ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/events/_execution.py
RENAMED
|
File without changes
|
{ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/events/emitter.py
RENAMED
|
File without changes
|
{ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/events/publisher.py
RENAMED
|
File without changes
|
{ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/events/router.py
RENAMED
|
File without changes
|
{ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/events/types.py
RENAMED
|
File without changes
|
{ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/models/__init__.py
RENAMED
|
File without changes
|
{ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/models/_base.py
RENAMED
|
File without changes
|
{ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/models/account.py
RENAMED
|
File without changes
|
|
File without changes
|
{ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/models/market_data.py
RENAMED
|
File without changes
|
{ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/models/order.py
RENAMED
|
File without changes
|
{ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/models/position.py
RENAMED
|
File without changes
|
{ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/src/ctrader_api_client/models/requests.py
RENAMED
|
File without changes
|
{ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/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.3 → ctrader_api_client-0.8.0}/tests/behavior/api/test_market_data.py
RENAMED
|
File without changes
|
{ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/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.3 → ctrader_api_client-0.8.0}/tests/behavior/auth/test_authentication.py
RENAMED
|
File without changes
|
{ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/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.3 → ctrader_api_client-0.8.0}/tests/behavior/connection/test_framing.py
RENAMED
|
File without changes
|
{ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/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.3 → ctrader_api_client-0.8.0}/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
|
|
File without changes
|
{ctrader_api_client-0.7.3 → ctrader_api_client-0.8.0}/tests/unit/test_market_data_conversion.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|