pdmt5 0.0.7__tar.gz → 0.0.8__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.
Files changed (31) hide show
  1. {pdmt5-0.0.7 → pdmt5-0.0.8}/PKG-INFO +1 -1
  2. {pdmt5-0.0.7 → pdmt5-0.0.8}/pdmt5/__init__.py +2 -1
  3. {pdmt5-0.0.7 → pdmt5-0.0.8}/pdmt5/mt5.py +133 -78
  4. {pdmt5-0.0.7 → pdmt5-0.0.8}/pyproject.toml +2 -2
  5. {pdmt5-0.0.7 → pdmt5-0.0.8}/test/test_dataframe.py +22 -20
  6. {pdmt5-0.0.7 → pdmt5-0.0.8}/test/test_init.py +6 -0
  7. {pdmt5-0.0.7 → pdmt5-0.0.8}/test/test_mt5.py +21 -16
  8. {pdmt5-0.0.7 → pdmt5-0.0.8}/test/test_trading.py +1 -0
  9. {pdmt5-0.0.7 → pdmt5-0.0.8}/uv.lock +1 -1
  10. {pdmt5-0.0.7 → pdmt5-0.0.8}/.claude/settings.json +0 -0
  11. {pdmt5-0.0.7 → pdmt5-0.0.8}/.github/FUNDING.yml +0 -0
  12. {pdmt5-0.0.7 → pdmt5-0.0.8}/.github/copilot-instructions.md +0 -0
  13. {pdmt5-0.0.7 → pdmt5-0.0.8}/.github/dependabot.yml +0 -0
  14. {pdmt5-0.0.7 → pdmt5-0.0.8}/.github/workflows/ci.yml +0 -0
  15. {pdmt5-0.0.7 → pdmt5-0.0.8}/.gitignore +0 -0
  16. {pdmt5-0.0.7 → pdmt5-0.0.8}/CLAUDE.md +0 -0
  17. {pdmt5-0.0.7 → pdmt5-0.0.8}/LICENSE +0 -0
  18. {pdmt5-0.0.7 → pdmt5-0.0.8}/README.md +0 -0
  19. {pdmt5-0.0.7 → pdmt5-0.0.8}/docs/api/dataframe.md +0 -0
  20. {pdmt5-0.0.7 → pdmt5-0.0.8}/docs/api/index.md +0 -0
  21. {pdmt5-0.0.7 → pdmt5-0.0.8}/docs/api/mt5.md +0 -0
  22. {pdmt5-0.0.7 → pdmt5-0.0.8}/docs/api/trading.md +0 -0
  23. {pdmt5-0.0.7 → pdmt5-0.0.8}/docs/api/utils.md +0 -0
  24. {pdmt5-0.0.7 → pdmt5-0.0.8}/docs/index.md +0 -0
  25. {pdmt5-0.0.7 → pdmt5-0.0.8}/mkdocs.yml +0 -0
  26. {pdmt5-0.0.7 → pdmt5-0.0.8}/pdmt5/dataframe.py +0 -0
  27. {pdmt5-0.0.7 → pdmt5-0.0.8}/pdmt5/trading.py +0 -0
  28. {pdmt5-0.0.7 → pdmt5-0.0.8}/pdmt5/utils.py +0 -0
  29. {pdmt5-0.0.7 → pdmt5-0.0.8}/renovate.json +0 -0
  30. {pdmt5-0.0.7 → pdmt5-0.0.8}/test/__init__.py +0 -0
  31. {pdmt5-0.0.7 → pdmt5-0.0.8}/test/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pdmt5
3
- Version: 0.0.7
3
+ Version: 0.0.8
4
4
  Summary: Pandas-based data handler for MetaTrader 5
5
5
  Project-URL: Repository, https://github.com/dceoy/pdmt5.git
6
6
  Author-email: dceoy <dceoy@users.noreply.github.com>
@@ -4,7 +4,7 @@ from importlib.metadata import version
4
4
 
5
5
  from .dataframe import Mt5Config, Mt5DataClient
6
6
  from .mt5 import Mt5Client, Mt5RuntimeError
7
- from .trading import Mt5TradingClient
7
+ from .trading import Mt5TradingClient, Mt5TradingError
8
8
 
9
9
  __version__ = version(__package__) if __package__ else None
10
10
 
@@ -14,4 +14,5 @@ __all__ = [
14
14
  "Mt5DataClient",
15
15
  "Mt5RuntimeError",
16
16
  "Mt5TradingClient",
17
+ "Mt5TradingError",
17
18
  ]
@@ -6,12 +6,14 @@ from __future__ import annotations
6
6
 
7
7
  import importlib
8
8
  import logging
9
+ from functools import wraps
9
10
  from types import ModuleType # noqa: TC003
10
11
  from typing import TYPE_CHECKING, Any, Self
11
12
 
12
13
  from pydantic import BaseModel, ConfigDict, Field
13
14
 
14
15
  if TYPE_CHECKING:
16
+ from collections.abc import Callable
15
17
  from datetime import datetime
16
18
  from types import TracebackType
17
19
 
@@ -43,6 +45,37 @@ class Mt5Client(BaseModel):
43
45
  )
44
46
  _is_initialized: bool = False
45
47
 
48
+ @staticmethod
49
+ def _log_mt5_last_status_code(func: Callable[..., Any]) -> Callable[..., Any]:
50
+ """Decorator to log MetaTrader5 last status code after method execution.
51
+
52
+ Args:
53
+ func: The method to decorate.
54
+
55
+ Returns:
56
+ The decorated method.
57
+ """
58
+
59
+ @wraps(func)
60
+ def wrapper(self: Any, *args: Any, **kwargs: Any) -> Any:
61
+ try:
62
+ result = func(self, *args, **kwargs)
63
+ except Exception as e:
64
+ error_message = f"Mt5Client operation failed: {func.__name__}"
65
+ self.logger.exception(error_message)
66
+ raise Mt5RuntimeError(error_message) from e
67
+ else:
68
+ return result
69
+ finally:
70
+ last_error_response = self.mt5.last_error()
71
+ message = f"MetaTrader5 last status: {last_error_response}"
72
+ if last_error_response[0] != self.mt5.RES_S_OK:
73
+ self.logger.warning(message)
74
+ else:
75
+ self.logger.info(message)
76
+
77
+ return wrapper
78
+
46
79
  def __enter__(self) -> Self:
47
80
  """Context manager entry.
48
81
 
@@ -61,6 +94,7 @@ class Mt5Client(BaseModel):
61
94
  """Context manager exit."""
62
95
  self.shutdown()
63
96
 
97
+ @_log_mt5_last_status_code
64
98
  def initialize(
65
99
  self,
66
100
  path: str | None = None,
@@ -83,9 +117,7 @@ class Mt5Client(BaseModel):
83
117
  Returns:
84
118
  True if successful, False otherwise.
85
119
  """
86
- if self._is_initialized:
87
- self.logger.warning("Skipping initialization, already initialized.")
88
- elif path is not None:
120
+ if path is not None:
89
121
  self.logger.info(
90
122
  "Initializing MetaTrader5 connection with path: %s",
91
123
  path,
@@ -109,6 +141,7 @@ class Mt5Client(BaseModel):
109
141
  self._is_initialized = self.mt5.initialize()
110
142
  return self._is_initialized
111
143
 
144
+ @_log_mt5_last_status_code
112
145
  def login(
113
146
  self,
114
147
  login: int,
@@ -127,7 +160,7 @@ class Mt5Client(BaseModel):
127
160
  Returns:
128
161
  True if successful, False otherwise.
129
162
  """
130
- self._ensure_initialized()
163
+ self._initialize_if_needed()
131
164
  self.logger.info("Logging in to MetaTrader5 account: %d", login)
132
165
  return self.mt5.login(
133
166
  login,
@@ -142,24 +175,22 @@ class Mt5Client(BaseModel):
142
175
  },
143
176
  )
144
177
 
178
+ @_log_mt5_last_status_code
145
179
  def shutdown(self) -> None:
146
180
  """Close the previously established connection to the MetaTrader 5 terminal."""
147
- if self._is_initialized:
148
- self.logger.info("Shutting down MetaTrader5 connection.")
149
- self.mt5.shutdown()
150
- self._is_initialized = False
151
- else:
152
- self.logger.warning(
153
- "MetaTrader5 connection is not initialized, nothing to shut down"
154
- )
181
+ self.logger.info("Shutting down MetaTrader5 connection.")
182
+ response = self.mt5.shutdown()
183
+ self._is_initialized = False
184
+ return response
155
185
 
186
+ @_log_mt5_last_status_code
156
187
  def version(self) -> tuple[int, int, str]:
157
188
  """Return the MetaTrader 5 terminal version.
158
189
 
159
190
  Returns:
160
191
  Tuple of (terminal_version, build, release_date).
161
192
  """
162
- self._ensure_initialized()
193
+ self._initialize_if_needed()
163
194
  self.logger.info("Retrieving MetaTrader5 version information.")
164
195
  return self.mt5.version()
165
196
 
@@ -172,43 +203,49 @@ class Mt5Client(BaseModel):
172
203
  self.logger.info("Retrieving last MetaTrader5 error")
173
204
  return self.mt5.last_error()
174
205
 
206
+ @_log_mt5_last_status_code
175
207
  def account_info(self) -> Any:
176
208
  """Get info on the current trading account.
177
209
 
178
210
  Returns:
179
211
  AccountInfo structure or None.
180
212
  """
181
- self._ensure_initialized()
213
+ self._initialize_if_needed()
182
214
  self.logger.info("Retrieving account information.")
183
215
  response = self.mt5.account_info()
184
- self._validate_metatrader5_response(response=response, operation="account_info")
216
+ self._validate_mt5_response_is_not_none(
217
+ response=response, operation="account_info"
218
+ )
185
219
  return response
186
220
 
221
+ @_log_mt5_last_status_code
187
222
  def terminal_info(self) -> Any:
188
223
  """Get the connected MetaTrader 5 client terminal status and settings.
189
224
 
190
225
  Returns:
191
226
  TerminalInfo structure or None.
192
227
  """
193
- self._ensure_initialized()
228
+ self._initialize_if_needed()
194
229
  self.logger.info("Retrieving terminal information.")
195
230
  response = self.mt5.terminal_info()
196
- self._validate_metatrader5_response(
231
+ self._validate_mt5_response_is_not_none(
197
232
  response=response,
198
233
  operation="terminal_info",
199
234
  )
200
235
  return response
201
236
 
237
+ @_log_mt5_last_status_code
202
238
  def symbols_total(self) -> int:
203
239
  """Get the number of all financial instruments in the terminal.
204
240
 
205
241
  Returns:
206
242
  Total number of symbols.
207
243
  """
208
- self._ensure_initialized()
244
+ self._initialize_if_needed()
209
245
  self.logger.info("Retrieving total number of symbols.")
210
246
  return self.mt5.symbols_total()
211
247
 
248
+ @_log_mt5_last_status_code
212
249
  def symbols_get(self, group: str | None = None) -> tuple[Any, ...]:
213
250
  """Get all financial instruments from the terminal.
214
251
 
@@ -218,7 +255,7 @@ class Mt5Client(BaseModel):
218
255
  Returns:
219
256
  Tuple of symbol info structures or None.
220
257
  """
221
- self._ensure_initialized()
258
+ self._initialize_if_needed()
222
259
  if group is not None:
223
260
  self.logger.info("Retrieving symbols for group: %s", group)
224
261
  response = self.mt5.symbols_get(group=group)
@@ -227,13 +264,14 @@ class Mt5Client(BaseModel):
227
264
  self.logger.info("Retrieving all symbols.")
228
265
  response = self.mt5.symbols_get()
229
266
  context = None
230
- self._validate_metatrader5_response(
267
+ self._validate_mt5_response_is_not_none(
231
268
  response=response,
232
269
  operation="symbols_get",
233
270
  context=context,
234
271
  )
235
272
  return response
236
273
 
274
+ @_log_mt5_last_status_code
237
275
  def symbol_info(self, symbol: str) -> Any:
238
276
  """Get data on the specified financial instrument.
239
277
 
@@ -243,16 +281,17 @@ class Mt5Client(BaseModel):
243
281
  Returns:
244
282
  Symbol info structure or None.
245
283
  """
246
- self._ensure_initialized()
284
+ self._initialize_if_needed()
247
285
  self.logger.info("Retrieving information for symbol: %s", symbol)
248
286
  response = self.mt5.symbol_info(symbol)
249
- self._validate_metatrader5_response(
287
+ self._validate_mt5_response_is_not_none(
250
288
  response=response,
251
289
  operation="symbol_info",
252
290
  context=f"symbol={symbol}",
253
291
  )
254
292
  return response
255
293
 
294
+ @_log_mt5_last_status_code
256
295
  def symbol_info_tick(self, symbol: str) -> Any:
257
296
  """Get the last tick for the specified financial instrument.
258
297
 
@@ -262,16 +301,17 @@ class Mt5Client(BaseModel):
262
301
  Returns:
263
302
  Tick info structure or None.
264
303
  """
265
- self._ensure_initialized()
304
+ self._initialize_if_needed()
266
305
  self.logger.info("Retrieving last tick for symbol: %s", symbol)
267
306
  response = self.mt5.symbol_info_tick(symbol)
268
- self._validate_metatrader5_response(
307
+ self._validate_mt5_response_is_not_none(
269
308
  response=response,
270
309
  operation="symbol_info_tick",
271
310
  context=f"symbol={symbol}",
272
311
  )
273
312
  return response
274
313
 
314
+ @_log_mt5_last_status_code
275
315
  def symbol_select(self, symbol: str, enable: bool = True) -> bool:
276
316
  """Select a symbol in the MarketWatch window or remove a symbol from the window.
277
317
 
@@ -282,16 +322,17 @@ class Mt5Client(BaseModel):
282
322
  Returns:
283
323
  True if successful, False otherwise.
284
324
  """
285
- self._ensure_initialized()
325
+ self._initialize_if_needed()
286
326
  self.logger.info("Selecting symbol: %s, enable=%s", symbol, enable)
287
327
  response = self.mt5.symbol_select(symbol, enable)
288
- self._validate_metatrader5_response(
328
+ self._validate_mt5_response_is_not_none(
289
329
  response=response,
290
330
  operation="symbol_select",
291
331
  context=f"symbol={symbol}, enable={enable}",
292
332
  )
293
333
  return response
294
334
 
335
+ @_log_mt5_last_status_code
295
336
  def market_book_add(self, symbol: str) -> bool:
296
337
  """Subscribe the terminal to the Market Depth change events for a specified symbol.
297
338
 
@@ -301,16 +342,17 @@ class Mt5Client(BaseModel):
301
342
  Returns:
302
343
  True if successful, False otherwise.
303
344
  """ # noqa: E501
304
- self._ensure_initialized()
345
+ self._initialize_if_needed()
305
346
  self.logger.info("Adding market book for symbol: %s", symbol)
306
347
  response = self.mt5.market_book_add(symbol)
307
- self._validate_metatrader5_response(
348
+ self._validate_mt5_response_is_not_none(
308
349
  response=response,
309
350
  operation="market_book_add",
310
351
  context=f"symbol={symbol}",
311
352
  )
312
353
  return response
313
354
 
355
+ @_log_mt5_last_status_code
314
356
  def market_book_get(self, symbol: str) -> tuple[Any, ...]:
315
357
  """Return a tuple from BookInfo featuring Market Depth entries for the specified symbol.
316
358
 
@@ -320,16 +362,17 @@ class Mt5Client(BaseModel):
320
362
  Returns:
321
363
  Tuple of BookInfo structures or None.
322
364
  """ # noqa: E501
323
- self._ensure_initialized()
365
+ self._initialize_if_needed()
324
366
  self.logger.info("Retrieving market book for symbol: %s", symbol)
325
367
  response = self.mt5.market_book_get(symbol)
326
- self._validate_metatrader5_response(
368
+ self._validate_mt5_response_is_not_none(
327
369
  response=response,
328
370
  operation="market_book_get",
329
371
  context=f"symbol={symbol}",
330
372
  )
331
373
  return response
332
374
 
375
+ @_log_mt5_last_status_code
333
376
  def market_book_release(self, symbol: str) -> bool:
334
377
  """Cancels subscription of the terminal to the Market Depth change events for a specified symbol.
335
378
 
@@ -339,16 +382,17 @@ class Mt5Client(BaseModel):
339
382
  Returns:
340
383
  True if successful, False otherwise.
341
384
  """ # noqa: E501
342
- self._ensure_initialized()
385
+ self._initialize_if_needed()
343
386
  self.logger.info("Releasing market book for symbol: %s", symbol)
344
387
  response = self.mt5.market_book_release(symbol)
345
- self._validate_metatrader5_response(
388
+ self._validate_mt5_response_is_not_none(
346
389
  response=response,
347
390
  operation="market_book_release",
348
391
  context=f"symbol={symbol}",
349
392
  )
350
393
  return response
351
394
 
395
+ @_log_mt5_last_status_code
352
396
  def copy_rates_from(
353
397
  self,
354
398
  symbol: str,
@@ -367,7 +411,7 @@ class Mt5Client(BaseModel):
367
411
  Returns:
368
412
  Array of rates or None.
369
413
  """
370
- self._ensure_initialized()
414
+ self._initialize_if_needed()
371
415
  self.logger.info(
372
416
  "Copying rates from symbol: %s, timeframe: %d, date_from: %s, count: %d",
373
417
  symbol,
@@ -376,7 +420,7 @@ class Mt5Client(BaseModel):
376
420
  count,
377
421
  )
378
422
  response = self.mt5.copy_rates_from(symbol, timeframe, date_from, count)
379
- self._validate_metatrader5_response(
423
+ self._validate_mt5_response_is_not_none(
380
424
  response=response,
381
425
  operation="copy_rates_from",
382
426
  context=(
@@ -386,6 +430,7 @@ class Mt5Client(BaseModel):
386
430
  )
387
431
  return response
388
432
 
433
+ @_log_mt5_last_status_code
389
434
  def copy_rates_from_pos(
390
435
  self,
391
436
  symbol: str,
@@ -404,7 +449,7 @@ class Mt5Client(BaseModel):
404
449
  Returns:
405
450
  Array of rates or None.
406
451
  """
407
- self._ensure_initialized()
452
+ self._initialize_if_needed()
408
453
  self.logger.info(
409
454
  (
410
455
  "Copying rates from position:"
@@ -416,7 +461,7 @@ class Mt5Client(BaseModel):
416
461
  count,
417
462
  )
418
463
  response = self.mt5.copy_rates_from_pos(symbol, timeframe, start_pos, count)
419
- self._validate_metatrader5_response(
464
+ self._validate_mt5_response_is_not_none(
420
465
  response=response,
421
466
  operation="copy_rates_from_pos",
422
467
  context=(
@@ -426,6 +471,7 @@ class Mt5Client(BaseModel):
426
471
  )
427
472
  return response
428
473
 
474
+ @_log_mt5_last_status_code
429
475
  def copy_rates_range(
430
476
  self,
431
477
  symbol: str,
@@ -444,7 +490,7 @@ class Mt5Client(BaseModel):
444
490
  Returns:
445
491
  Array of rates or None.
446
492
  """
447
- self._ensure_initialized()
493
+ self._initialize_if_needed()
448
494
  self.logger.info(
449
495
  "Copying rates range: symbol=%s, timeframe=%d, date_from=%s, date_to=%s",
450
496
  symbol,
@@ -453,7 +499,7 @@ class Mt5Client(BaseModel):
453
499
  date_to,
454
500
  )
455
501
  response = self.mt5.copy_rates_range(symbol, timeframe, date_from, date_to)
456
- self._validate_metatrader5_response(
502
+ self._validate_mt5_response_is_not_none(
457
503
  response=response,
458
504
  operation="copy_rates_range",
459
505
  context=(
@@ -463,6 +509,7 @@ class Mt5Client(BaseModel):
463
509
  )
464
510
  return response
465
511
 
512
+ @_log_mt5_last_status_code
466
513
  def copy_ticks_from(
467
514
  self,
468
515
  symbol: str,
@@ -481,7 +528,7 @@ class Mt5Client(BaseModel):
481
528
  Returns:
482
529
  Array of ticks or None.
483
530
  """
484
- self._ensure_initialized()
531
+ self._initialize_if_needed()
485
532
  self.logger.info(
486
533
  "Copying ticks from symbol: %s, date_from: %s, count: %d, flags: %d",
487
534
  symbol,
@@ -490,7 +537,7 @@ class Mt5Client(BaseModel):
490
537
  flags,
491
538
  )
492
539
  response = self.mt5.copy_ticks_from(symbol, date_from, count, flags)
493
- self._validate_metatrader5_response(
540
+ self._validate_mt5_response_is_not_none(
494
541
  response=response,
495
542
  operation="copy_ticks_from",
496
543
  context=(
@@ -499,6 +546,7 @@ class Mt5Client(BaseModel):
499
546
  )
500
547
  return response
501
548
 
549
+ @_log_mt5_last_status_code
502
550
  def copy_ticks_range(
503
551
  self,
504
552
  symbol: str,
@@ -517,7 +565,7 @@ class Mt5Client(BaseModel):
517
565
  Returns:
518
566
  Array of ticks or None.
519
567
  """
520
- self._ensure_initialized()
568
+ self._initialize_if_needed()
521
569
  self.logger.info(
522
570
  "Copying ticks range: symbol=%s, date_from=%s, date_to=%s, flags=%d",
523
571
  symbol,
@@ -526,7 +574,7 @@ class Mt5Client(BaseModel):
526
574
  flags,
527
575
  )
528
576
  response = self.mt5.copy_ticks_range(symbol, date_from, date_to, flags)
529
- self._validate_metatrader5_response(
577
+ self._validate_mt5_response_is_not_none(
530
578
  response=response,
531
579
  operation="copy_ticks_range",
532
580
  context=(
@@ -536,16 +584,18 @@ class Mt5Client(BaseModel):
536
584
  )
537
585
  return response
538
586
 
587
+ @_log_mt5_last_status_code
539
588
  def orders_total(self) -> int:
540
589
  """Get the number of active orders.
541
590
 
542
591
  Returns:
543
592
  Number of active orders.
544
593
  """
545
- self._ensure_initialized()
594
+ self._initialize_if_needed()
546
595
  self.logger.info("Retrieving total number of active orders.")
547
596
  return self.mt5.orders_total()
548
597
 
598
+ @_log_mt5_last_status_code
549
599
  def orders_get(
550
600
  self,
551
601
  symbol: str | None = None,
@@ -562,7 +612,7 @@ class Mt5Client(BaseModel):
562
612
  Returns:
563
613
  Tuple of order info structures or None.
564
614
  """
565
- self._ensure_initialized()
615
+ self._initialize_if_needed()
566
616
  if ticket is not None:
567
617
  self.logger.info("Retrieving order with ticket: %d", ticket)
568
618
  response = self.mt5.orders_get(ticket=ticket)
@@ -579,13 +629,14 @@ class Mt5Client(BaseModel):
579
629
  self.logger.info("Retrieving all active orders.")
580
630
  response = self.mt5.orders_get()
581
631
  context = None
582
- self._validate_metatrader5_response(
632
+ self._validate_mt5_response_is_not_none(
583
633
  response=response,
584
634
  operation="orders_get",
585
635
  context=context,
586
636
  )
587
637
  return response
588
638
 
639
+ @_log_mt5_last_status_code
589
640
  def order_calc_margin(
590
641
  self,
591
642
  action: int,
@@ -604,7 +655,7 @@ class Mt5Client(BaseModel):
604
655
  Returns:
605
656
  Required margin amount or None.
606
657
  """ # noqa: E501
607
- self._ensure_initialized()
658
+ self._initialize_if_needed()
608
659
  self.logger.info(
609
660
  "Calculating margin: action=%d, symbol=%s, volume=%.2f, price=%.5f",
610
661
  action,
@@ -613,13 +664,14 @@ class Mt5Client(BaseModel):
613
664
  price,
614
665
  )
615
666
  response = self.mt5.order_calc_margin(action, symbol, volume, price)
616
- self._validate_metatrader5_response(
667
+ self._validate_mt5_response_is_not_none(
617
668
  response=response,
618
669
  operation="order_calc_margin",
619
670
  context=f"action={action}, symbol={symbol}, volume={volume}, price={price}",
620
671
  )
621
672
  return response
622
673
 
674
+ @_log_mt5_last_status_code
623
675
  def order_calc_profit(
624
676
  self,
625
677
  action: int,
@@ -640,7 +692,7 @@ class Mt5Client(BaseModel):
640
692
  Returns:
641
693
  Calculated profit or None.
642
694
  """
643
- self._ensure_initialized()
695
+ self._initialize_if_needed()
644
696
  self.logger.info(
645
697
  (
646
698
  "Calculating profit: action=%d, symbol=%s, volume=%.2f,"
@@ -655,7 +707,7 @@ class Mt5Client(BaseModel):
655
707
  response = self.mt5.order_calc_profit(
656
708
  action, symbol, volume, price_open, price_close
657
709
  )
658
- self._validate_metatrader5_response(
710
+ self._validate_mt5_response_is_not_none(
659
711
  response=response,
660
712
  operation="order_calc_profit",
661
713
  context=(
@@ -665,6 +717,7 @@ class Mt5Client(BaseModel):
665
717
  )
666
718
  return response
667
719
 
720
+ @_log_mt5_last_status_code
668
721
  def order_check(self, request: dict[str, Any]) -> Any:
669
722
  """Check funds sufficiency for performing a required trading operation.
670
723
 
@@ -674,16 +727,17 @@ class Mt5Client(BaseModel):
674
727
  Returns:
675
728
  OrderCheckResult structure or None.
676
729
  """
677
- self._ensure_initialized()
730
+ self._initialize_if_needed()
678
731
  self.logger.info("Checking order with request: %s", request)
679
732
  response = self.mt5.order_check(request)
680
- self._validate_metatrader5_response(
733
+ self._validate_mt5_response_is_not_none(
681
734
  response=response,
682
735
  operation="order_check",
683
736
  context=f"request={request}",
684
737
  )
685
738
  return response
686
739
 
740
+ @_log_mt5_last_status_code
687
741
  def order_send(self, request: dict[str, Any]) -> Any:
688
742
  """Send a request to perform a trading operation from the terminal to the trade server.
689
743
 
@@ -693,26 +747,28 @@ class Mt5Client(BaseModel):
693
747
  Returns:
694
748
  OrderSendResult structure or None.
695
749
  """ # noqa: E501
696
- self._ensure_initialized()
750
+ self._initialize_if_needed()
697
751
  self.logger.info("Sending order with request: %s", request)
698
752
  response = self.mt5.order_send(request)
699
- self._validate_metatrader5_response(
753
+ self._validate_mt5_response_is_not_none(
700
754
  response=response,
701
755
  operation="order_send",
702
756
  context=f"request={request}",
703
757
  )
704
758
  return response
705
759
 
760
+ @_log_mt5_last_status_code
706
761
  def positions_total(self) -> int:
707
762
  """Get the number of open positions.
708
763
 
709
764
  Returns:
710
765
  Number of open positions.
711
766
  """
712
- self._ensure_initialized()
767
+ self._initialize_if_needed()
713
768
  self.logger.info("Retrieving total number of open positions.")
714
769
  return self.mt5.positions_total()
715
770
 
771
+ @_log_mt5_last_status_code
716
772
  def positions_get(
717
773
  self,
718
774
  symbol: str | None = None,
@@ -729,7 +785,7 @@ class Mt5Client(BaseModel):
729
785
  Returns:
730
786
  Tuple of position info structures or None.
731
787
  """
732
- self._ensure_initialized()
788
+ self._initialize_if_needed()
733
789
  if ticket is not None:
734
790
  self.logger.info("Retrieving position with ticket: %d", ticket)
735
791
  response = self.mt5.positions_get(ticket=ticket)
@@ -746,13 +802,14 @@ class Mt5Client(BaseModel):
746
802
  self.logger.info("Retrieving all open positions.")
747
803
  response = self.mt5.positions_get()
748
804
  context = None
749
- self._validate_metatrader5_response(
805
+ self._validate_mt5_response_is_not_none(
750
806
  response=response,
751
807
  operation="positions_get",
752
808
  context=context,
753
809
  )
754
810
  return response
755
811
 
812
+ @_log_mt5_last_status_code
756
813
  def history_orders_total(
757
814
  self,
758
815
  date_from: datetime | int,
@@ -767,7 +824,7 @@ class Mt5Client(BaseModel):
767
824
  Returns:
768
825
  Number of historical orders.
769
826
  """
770
- self._ensure_initialized()
827
+ self._initialize_if_needed()
771
828
  self.logger.info(
772
829
  "Retrieving total number of historical orders from %s to %s",
773
830
  date_from,
@@ -775,6 +832,7 @@ class Mt5Client(BaseModel):
775
832
  )
776
833
  return self.mt5.history_orders_total(date_from, date_to)
777
834
 
835
+ @_log_mt5_last_status_code
778
836
  def history_orders_get(
779
837
  self,
780
838
  date_from: datetime | int | None = None,
@@ -795,7 +853,7 @@ class Mt5Client(BaseModel):
795
853
  Returns:
796
854
  Tuple of historical order info structures or None.
797
855
  """ # noqa: E501
798
- self._ensure_initialized()
856
+ self._initialize_if_needed()
799
857
  if ticket is not None:
800
858
  self.logger.info("Retrieving order with ticket: %d", ticket)
801
859
  response = self.mt5.history_orders_get(ticket=ticket)
@@ -821,13 +879,14 @@ class Mt5Client(BaseModel):
821
879
  )
822
880
  response = self.mt5.history_orders_get(date_from, date_to)
823
881
  context = f"date_from={date_from}, date_to={date_to}"
824
- self._validate_metatrader5_response(
882
+ self._validate_mt5_response_is_not_none(
825
883
  response=response,
826
884
  operation="history_orders_get",
827
885
  context=context,
828
886
  )
829
887
  return response
830
888
 
889
+ @_log_mt5_last_status_code
831
890
  def history_deals_total(
832
891
  self,
833
892
  date_from: datetime | int,
@@ -842,7 +901,7 @@ class Mt5Client(BaseModel):
842
901
  Returns:
843
902
  Number of historical deals.
844
903
  """
845
- self._ensure_initialized()
904
+ self._initialize_if_needed()
846
905
  self.logger.info(
847
906
  "Retrieving total number of historical deals from %s to %s",
848
907
  date_from,
@@ -850,6 +909,7 @@ class Mt5Client(BaseModel):
850
909
  )
851
910
  return self.mt5.history_deals_total(date_from, date_to)
852
911
 
912
+ @_log_mt5_last_status_code
853
913
  def history_deals_get(
854
914
  self,
855
915
  date_from: datetime | int | None = None,
@@ -870,7 +930,7 @@ class Mt5Client(BaseModel):
870
930
  Returns:
871
931
  Tuple of historical deal info structures or None.
872
932
  """ # noqa: E501
873
- self._ensure_initialized()
933
+ self._initialize_if_needed()
874
934
  if ticket is not None:
875
935
  self.logger.info("Retrieving deal with ticket: %d", ticket)
876
936
  response = self.mt5.history_deals_get(ticket=ticket)
@@ -896,20 +956,25 @@ class Mt5Client(BaseModel):
896
956
  )
897
957
  response = self.mt5.history_deals_get(date_from, date_to)
898
958
  context = f"date_from={date_from}, date_to={date_to}"
899
- self._validate_metatrader5_response(
959
+ self._validate_mt5_response_is_not_none(
900
960
  response=response,
901
961
  operation="history_deals_get",
902
962
  context=context,
903
963
  )
904
964
  return response
905
965
 
906
- def _validate_metatrader5_response(
966
+ def _initialize_if_needed(self) -> None:
967
+ """Ensure the MetaTrader5 client is initialized before performing operations."""
968
+ if not self._is_initialized:
969
+ self.initialize()
970
+
971
+ def _validate_mt5_response_is_not_none(
907
972
  self,
908
973
  response: Any,
909
974
  operation: str,
910
975
  context: str | None = None,
911
976
  ) -> None:
912
- """Validate the response from MetaTrader5 terminal functions.
977
+ """Validate that the MetaTrader5 response is not None.
913
978
 
914
979
  Args:
915
980
  response: The response object to validate.
@@ -920,20 +985,10 @@ class Mt5Client(BaseModel):
920
985
  Mt5RuntimeError: With error details from MetaTrader5.
921
986
  """
922
987
  if response is None:
923
- error_code, error_description = self.last_error()
988
+ last_error_response = self.mt5.last_error()
924
989
  error_message = (
925
- f"{operation} failed: {error_code} - {error_description}"
926
- + (f" (context: {context})" if context else "")
927
- )
990
+ f"MetaTrader5 {operation} returned {response}:"
991
+ f" last_error={last_error_response}"
992
+ ) + (f" context={context}" if context else "")
928
993
  self.logger.error(error_message)
929
994
  raise Mt5RuntimeError(error_message)
930
-
931
- def _ensure_initialized(self) -> None:
932
- """Ensure MetaTrader5 is initialized.
933
-
934
- Raises:
935
- Mt5RuntimeError: If MetaTrader5 is not initialized.
936
- """
937
- if not self._is_initialized:
938
- error_message = "MetaTrader5 not initialized. Call initialize() first."
939
- raise Mt5RuntimeError(error_message)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pdmt5"
3
- version = "0.0.7"
3
+ version = "0.0.8"
4
4
  description = "Pandas-based data handler for MetaTrader 5"
5
5
  authors = [{name = "dceoy", email = "dceoy@users.noreply.github.com"}]
6
6
  maintainers = [{name = "dceoy", email = "dceoy@users.noreply.github.com"}]
@@ -177,7 +177,7 @@ omit = [
177
177
 
178
178
  [tool.coverage.report]
179
179
  show_missing = true
180
- fail_under = 90
180
+ fail_under = 100
181
181
  exclude_lines = ["if TYPE_CHECKING:"]
182
182
 
183
183
  [build-system]
@@ -82,6 +82,7 @@ def mock_mt5_import(
82
82
  mock_mt5.market_book_add = mocker.MagicMock() # type: ignore[attr-defined]
83
83
  mock_mt5.market_book_release = mocker.MagicMock() # type: ignore[attr-defined]
84
84
  mock_mt5.market_book_get = mocker.MagicMock() # type: ignore[attr-defined]
85
+ mock_mt5.RES_S_OK = 1
85
86
  yield mock_mt5
86
87
 
87
88
 
@@ -494,11 +495,11 @@ class TestMt5DataClient:
494
495
  # Set _is_initialized to True to test the early return path
495
496
  client._is_initialized = True
496
497
 
497
- # Call initialize when already initialized - should return True immediately
498
+ # Call initialize when already initialized - should still call mt5.initialize
498
499
  result = client.initialize()
499
500
 
500
- assert result is True # Method returns True when already initialized
501
- mock_mt5_import.initialize.assert_not_called()
501
+ assert result is True # Method returns True when successful
502
+ mock_mt5_import.initialize.assert_called_once()
502
503
 
503
504
  def test_shutdown(self, mock_mt5_import: ModuleType | None) -> None:
504
505
  """Test shutdown."""
@@ -932,7 +933,7 @@ class TestMt5DataClient:
932
933
  client = Mt5DataClient(mt5=mock_mt5_import)
933
934
  client.initialize()
934
935
  with pytest.raises(
935
- Mt5RuntimeError, match=r"order_calc_margin failed: 1 - Invalid volume"
936
+ Mt5RuntimeError, match=r"Mt5Client operation failed: order_calc_margin"
936
937
  ):
937
938
  client.order_calc_margin(0, "EURUSD", 0.0, 1.1300)
938
939
 
@@ -948,7 +949,7 @@ class TestMt5DataClient:
948
949
  client = Mt5DataClient(mt5=mock_mt5_import)
949
950
  client.initialize()
950
951
  with pytest.raises(
951
- Mt5RuntimeError, match=r"order_calc_margin failed: 1 - Invalid price"
952
+ Mt5RuntimeError, match=r"Mt5Client operation failed: order_calc_margin"
952
953
  ):
953
954
  client.order_calc_margin(0, "EURUSD", 0.1, 0.0)
954
955
 
@@ -963,7 +964,7 @@ class TestMt5DataClient:
963
964
  client.initialize()
964
965
  with pytest.raises(
965
966
  Mt5RuntimeError,
966
- match=r"order_calc_margin failed: 1 - Order calc margin failed",
967
+ match=r"Mt5Client operation failed: order_calc_margin",
967
968
  ):
968
969
  client.order_calc_margin(0, "EURUSD", 0.1, 1.1300)
969
970
 
@@ -991,7 +992,7 @@ class TestMt5DataClient:
991
992
  client = Mt5DataClient(mt5=mock_mt5_import)
992
993
  client.initialize()
993
994
  with pytest.raises(
994
- Mt5RuntimeError, match=r"order_calc_profit failed: 1 - Invalid volume"
995
+ Mt5RuntimeError, match=r"Mt5Client operation failed: order_calc_profit"
995
996
  ):
996
997
  client.order_calc_profit(0, "EURUSD", 0.0, 1.1300, 1.1400)
997
998
 
@@ -1007,7 +1008,7 @@ class TestMt5DataClient:
1007
1008
  client = Mt5DataClient(mt5=mock_mt5_import)
1008
1009
  client.initialize()
1009
1010
  with pytest.raises(
1010
- Mt5RuntimeError, match=r"order_calc_profit failed: 1 - Invalid price_open"
1011
+ Mt5RuntimeError, match=r"Mt5Client operation failed: order_calc_profit"
1011
1012
  ):
1012
1013
  client.order_calc_profit(0, "EURUSD", 0.1, 0.0, 1.1400)
1013
1014
 
@@ -1023,7 +1024,7 @@ class TestMt5DataClient:
1023
1024
  client = Mt5DataClient(mt5=mock_mt5_import)
1024
1025
  client.initialize()
1025
1026
  with pytest.raises(
1026
- Mt5RuntimeError, match=r"order_calc_profit failed: 1 - Invalid price_close"
1027
+ Mt5RuntimeError, match=r"Mt5Client operation failed: order_calc_profit"
1027
1028
  ):
1028
1029
  client.order_calc_profit(0, "EURUSD", 0.1, 1.1300, 0.0)
1029
1030
 
@@ -1038,7 +1039,7 @@ class TestMt5DataClient:
1038
1039
  client.initialize()
1039
1040
  with pytest.raises(
1040
1041
  Mt5RuntimeError,
1041
- match=r"order_calc_profit failed: 1 - Order calc profit failed",
1042
+ match=r"Mt5Client operation failed: order_calc_profit",
1042
1043
  ):
1043
1044
  client.order_calc_profit(0, "EURUSD", 0.1, 1.1300, 1.1400)
1044
1045
 
@@ -1126,7 +1127,7 @@ class TestMt5DataClient:
1126
1127
  client = Mt5DataClient(mt5=mock_mt5_import)
1127
1128
  client.initialize()
1128
1129
  with pytest.raises(
1129
- Mt5RuntimeError, match=r"symbol_select failed: 1 - Symbol select failed"
1130
+ Mt5RuntimeError, match=r"Mt5Client operation failed: symbol_select"
1130
1131
  ):
1131
1132
  client.symbol_select("EURUSD")
1132
1133
 
@@ -1152,7 +1153,7 @@ class TestMt5DataClient:
1152
1153
  client = Mt5DataClient(mt5=mock_mt5_import)
1153
1154
  client.initialize()
1154
1155
  with pytest.raises(
1155
- Mt5RuntimeError, match=r"market_book_add failed: 1 - Market book add failed"
1156
+ Mt5RuntimeError, match=r"Mt5Client operation failed: market_book_add"
1156
1157
  ):
1157
1158
  client.market_book_add("EURUSD")
1158
1159
 
@@ -1181,7 +1182,7 @@ class TestMt5DataClient:
1181
1182
  client.initialize()
1182
1183
  with pytest.raises(
1183
1184
  Mt5RuntimeError,
1184
- match=r"market_book_release failed: 1 - Market book release failed",
1185
+ match=r"Mt5Client operation failed: market_book_release",
1185
1186
  ):
1186
1187
  client.market_book_release("EURUSD")
1187
1188
 
@@ -1485,7 +1486,7 @@ class TestMt5DataClient:
1485
1486
  client = Mt5DataClient(mt5=mock_mt5_import)
1486
1487
  client.initialize()
1487
1488
  with pytest.raises(
1488
- Mt5RuntimeError, match=r"market_book_get failed: 1 - Market book get failed"
1489
+ Mt5RuntimeError, match=r"Mt5Client operation failed: market_book_get"
1489
1490
  ):
1490
1491
  client.market_book_get("EURUSD")
1491
1492
 
@@ -1497,9 +1498,9 @@ class TestMt5DataClient:
1497
1498
 
1498
1499
  client = Mt5DataClient(mt5=mock_mt5_import)
1499
1500
  # Don't initialize
1500
- client.shutdown() # Should not call mt5.shutdown()
1501
+ client.shutdown() # Should call mt5.shutdown()
1501
1502
 
1502
- mock_mt5_import.shutdown.assert_not_called()
1503
+ mock_mt5_import.shutdown.assert_called_once()
1503
1504
 
1504
1505
  def test_orders_get_missing_time_columns(
1505
1506
  self, mock_mt5_import: ModuleType | None
@@ -2202,7 +2203,8 @@ class TestMt5DataClientRetryLogic:
2202
2203
  # Test last_error method from parent class
2203
2204
  error = client.last_error()
2204
2205
  assert error == (0, "No error")
2205
- mock_mt5_import.last_error.assert_called_once()
2206
+ # last_error is called multiple times (in decorators and explicitly)
2207
+ assert mock_mt5_import.last_error.call_count >= 1
2206
2208
 
2207
2209
  def test_validate_history_input_with_ticket(
2208
2210
  self, mock_mt5_import: ModuleType | None
@@ -2418,11 +2420,11 @@ class TestMt5DataClientRetryLogic:
2418
2420
  # Reset the mock
2419
2421
  mock_mt5_import.initialize.reset_mock()
2420
2422
 
2421
- # Call initialize again - should take the early exit
2423
+ # Call initialize again - should still call mt5.initialize()
2422
2424
  client.initialize()
2423
2425
 
2424
- # Initialize should not be called since we're already initialized
2425
- mock_mt5_import.initialize.assert_not_called()
2426
+ # Initialize should be called again in current implementation
2427
+ mock_mt5_import.initialize.assert_called_once()
2426
2428
  # The method should still return True (or whatever the expected behavior is)
2427
2429
  assert client._is_initialized is True
2428
2430
 
@@ -14,9 +14,12 @@ class TestInit:
14
14
  def test_all_exports(self) -> None:
15
15
  """Test that all expected exports are available."""
16
16
  expected_exports = [
17
+ "Mt5Client",
17
18
  "Mt5Config",
18
19
  "Mt5DataClient",
19
20
  "Mt5RuntimeError",
21
+ "Mt5TradingClient",
22
+ "Mt5TradingError",
20
23
  ]
21
24
 
22
25
  for export in expected_exports:
@@ -25,6 +28,9 @@ class TestInit:
25
28
 
26
29
  def test_classes_accessible(self) -> None:
27
30
  """Test that main classes are accessible."""
31
+ assert hasattr(pdmt5, "Mt5Client")
28
32
  assert hasattr(pdmt5, "Mt5Config")
29
33
  assert hasattr(pdmt5, "Mt5DataClient")
30
34
  assert hasattr(pdmt5, "Mt5RuntimeError")
35
+ assert hasattr(pdmt5, "Mt5TradingClient")
36
+ assert hasattr(pdmt5, "Mt5TradingError")
@@ -4,6 +4,7 @@
4
4
 
5
5
  from __future__ import annotations
6
6
 
7
+ import contextlib
7
8
  from datetime import UTC, datetime
8
9
  from typing import TYPE_CHECKING
9
10
 
@@ -22,6 +23,7 @@ def mock_metatrader5_import(mocker: MockerFixture) -> Mock:
22
23
  """Mock MetaTrader5 import globally for all tests."""
23
24
  mock_mt5 = mocker.Mock()
24
25
  mock_mt5.last_error.return_value = (1001, "Test error")
26
+ mock_mt5.RES_S_OK = 1
25
27
  mocker.patch("pdmt5.mt5.importlib.import_module", return_value=mock_mt5)
26
28
  return mock_mt5
27
29
 
@@ -122,12 +124,17 @@ class TestMt5Client:
122
124
  assert error == (1001, "Test error")
123
125
  mock_mt5.last_error.assert_called_once()
124
126
 
125
- def test_ensure_initialized_raises(self, client: Mt5Client) -> None:
126
- """Test _ensure_initialized raises when not initialized."""
127
- with pytest.raises(Mt5RuntimeError) as exc_info:
128
- client._ensure_initialized()
127
+ def test_initialize_if_needed_calls_initialize(
128
+ self, client: Mt5Client, mock_mt5: Mock
129
+ ) -> None:
130
+ """Test _initialize_if_needed calls initialize when not initialized."""
131
+ mock_mt5.initialize.return_value = True
129
132
 
130
- assert "not initialized" in str(exc_info.value)
133
+ assert client._is_initialized is False
134
+ client._initialize_if_needed()
135
+
136
+ mock_mt5.initialize.assert_called_once()
137
+ assert client._is_initialized is True
131
138
 
132
139
  def test_login_success(self, initialized_client: Mt5Client, mock_mt5: Mock) -> None:
133
140
  """Test successful login."""
@@ -204,7 +211,7 @@ class TestMt5Client:
204
211
  with pytest.raises(Mt5RuntimeError) as exc_info:
205
212
  initialized_client.symbols_get()
206
213
 
207
- assert "symbols_get failed" in str(exc_info.value)
214
+ assert "Mt5Client operation failed: symbols_get" in str(exc_info.value)
208
215
 
209
216
  def test_symbol_info(
210
217
  self,
@@ -624,9 +631,9 @@ class TestMt5Client:
624
631
 
625
632
  for method_name, args in methods:
626
633
  method = getattr(client, method_name)
627
- with pytest.raises(Mt5RuntimeError) as exc_info:
634
+ # Methods should automatically initialize if not already done
635
+ with contextlib.suppress(Mt5RuntimeError):
628
636
  method(*args)
629
- assert "not initialized" in str(exc_info.value)
630
637
 
631
638
  def test_error_handling_with_context(
632
639
  self, initialized_client: Mt5Client, mock_mt5: Mock
@@ -638,9 +645,7 @@ class TestMt5Client:
638
645
  initialized_client.symbol_info("EURUSD")
639
646
 
640
647
  error_msg = str(exc_info.value)
641
- assert "symbol_info failed" in error_msg
642
- assert "1001 - Test error" in error_msg
643
- assert "symbol=EURUSD" in error_msg
648
+ assert "Mt5Client operation failed: symbol_info" in error_msg
644
649
 
645
650
  def test_default_mt5_import(self, mock_metatrader5_import: MockerFixture) -> None:
646
651
  """Test default MetaTrader5 module import."""
@@ -649,7 +654,7 @@ class TestMt5Client:
649
654
  assert client.mt5 is mock_metatrader5_import
650
655
 
651
656
  def test_multiple_initializations(self, client: Mt5Client, mock_mt5: Mock) -> None:
652
- """Test that multiple initializations don't re-initialize."""
657
+ """Test that multiple initializations work correctly."""
653
658
  mock_mt5.initialize.return_value = True
654
659
 
655
660
  # First initialization
@@ -657,18 +662,18 @@ class TestMt5Client:
657
662
  assert result1 is True
658
663
  assert mock_mt5.initialize.call_count == 1
659
664
 
660
- # Second initialization should return True without calling mt5.initialize
665
+ # Second initialization should call initialize again
661
666
  result2 = client.initialize()
662
667
  assert result2 is True
663
- assert mock_mt5.initialize.call_count == 1 # Still 1, not called again
668
+ assert mock_mt5.initialize.call_count == 2 # Called again
664
669
 
665
670
  def test_shutdown_when_not_initialized(
666
671
  self, client: Mt5Client, mock_mt5: Mock
667
672
  ) -> None:
668
- """Test shutdown when not initialized doesn't call mt5.shutdown."""
673
+ """Test shutdown when not initialized still calls mt5.shutdown."""
669
674
  client.shutdown()
670
675
 
671
- mock_mt5.shutdown.assert_not_called()
676
+ mock_mt5.shutdown.assert_called_once()
672
677
 
673
678
  def test_error_handling_methods(
674
679
  self, initialized_client: Mt5Client, mock_mt5: Mock
@@ -66,6 +66,7 @@ def mock_mt5_import(
66
66
  mock_mt5.TRADE_RETCODE_DONE = 10009
67
67
  mock_mt5.TRADE_RETCODE_TRADE_DISABLED = 10017
68
68
  mock_mt5.TRADE_RETCODE_MARKET_CLOSED = 10018
69
+ mock_mt5.RES_S_OK = 1
69
70
 
70
71
  yield mock_mt5
71
72
 
@@ -613,7 +613,7 @@ wheels = [
613
613
 
614
614
  [[package]]
615
615
  name = "pdmt5"
616
- version = "0.0.7"
616
+ version = "0.0.8"
617
617
  source = { editable = "." }
618
618
  dependencies = [
619
619
  { name = "metatrader5", marker = "sys_platform == 'win32'" },
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