mt5cli 1.2.1__tar.gz → 1.2.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. {mt5cli-1.2.1 → mt5cli-1.2.3}/.github/workflows/ci.yml +15 -0
  2. {mt5cli-1.2.1 → mt5cli-1.2.3}/AGENTS.md +1 -1
  3. {mt5cli-1.2.1 → mt5cli-1.2.3}/PKG-INFO +3 -3
  4. {mt5cli-1.2.1 → mt5cli-1.2.3}/docs/api/trading.md +14 -9
  5. {mt5cli-1.2.1 → mt5cli-1.2.3}/mt5cli/cli.py +66 -37
  6. {mt5cli-1.2.1 → mt5cli-1.2.3}/mt5cli/client.py +25 -0
  7. {mt5cli-1.2.1 → mt5cli-1.2.3}/mt5cli/history.py +9 -1
  8. {mt5cli-1.2.1 → mt5cli-1.2.3}/mt5cli/trading.py +53 -10
  9. {mt5cli-1.2.1 → mt5cli-1.2.3}/pyproject.toml +3 -3
  10. {mt5cli-1.2.1 → mt5cli-1.2.3}/tests/test_cli.py +24 -0
  11. {mt5cli-1.2.1 → mt5cli-1.2.3}/tests/test_history.py +14 -3
  12. {mt5cli-1.2.1 → mt5cli-1.2.3}/tests/test_trading.py +100 -4
  13. {mt5cli-1.2.1 → mt5cli-1.2.3}/uv.lock +10 -10
  14. mt5cli-1.2.1/.github/workflows/opencode.yml +0 -72
  15. {mt5cli-1.2.1 → mt5cli-1.2.3}/.agents/skills/local-qa/SKILL.md +0 -0
  16. {mt5cli-1.2.1 → mt5cli-1.2.3}/.agents/skills/local-qa/scripts/qa.sh +0 -0
  17. {mt5cli-1.2.1 → mt5cli-1.2.3}/.agents/skills/mt5cli/SKILL.md +0 -0
  18. {mt5cli-1.2.1 → mt5cli-1.2.3}/.agents/skills/pr-feedback-triage/SKILL.md +0 -0
  19. {mt5cli-1.2.1 → mt5cli-1.2.3}/.claude/agents/codex.md +0 -0
  20. {mt5cli-1.2.1 → mt5cli-1.2.3}/.claude/settings.json +0 -0
  21. {mt5cli-1.2.1 → mt5cli-1.2.3}/.github/FUNDING.yml +0 -0
  22. {mt5cli-1.2.1 → mt5cli-1.2.3}/.github/dependabot.yml +0 -0
  23. {mt5cli-1.2.1 → mt5cli-1.2.3}/.github/renovate.json +0 -0
  24. {mt5cli-1.2.1 → mt5cli-1.2.3}/.github/workflows/claude.yml +0 -0
  25. {mt5cli-1.2.1 → mt5cli-1.2.3}/.github/workflows/release.yml +0 -0
  26. {mt5cli-1.2.1 → mt5cli-1.2.3}/.gitignore +0 -0
  27. {mt5cli-1.2.1 → mt5cli-1.2.3}/CLAUDE.md +0 -0
  28. {mt5cli-1.2.1 → mt5cli-1.2.3}/LICENSE +0 -0
  29. {mt5cli-1.2.1 → mt5cli-1.2.3}/README.md +0 -0
  30. {mt5cli-1.2.1 → mt5cli-1.2.3}/docs/api/cli.md +0 -0
  31. {mt5cli-1.2.1 → mt5cli-1.2.3}/docs/api/client.md +0 -0
  32. {mt5cli-1.2.1 → mt5cli-1.2.3}/docs/api/converters.md +0 -0
  33. {mt5cli-1.2.1 → mt5cli-1.2.3}/docs/api/exceptions.md +0 -0
  34. {mt5cli-1.2.1 → mt5cli-1.2.3}/docs/api/grafana.md +0 -0
  35. {mt5cli-1.2.1 → mt5cli-1.2.3}/docs/api/history.md +0 -0
  36. {mt5cli-1.2.1 → mt5cli-1.2.3}/docs/api/index.md +0 -0
  37. {mt5cli-1.2.1 → mt5cli-1.2.3}/docs/api/marketdata.md +0 -0
  38. {mt5cli-1.2.1 → mt5cli-1.2.3}/docs/api/observability.md +0 -0
  39. {mt5cli-1.2.1 → mt5cli-1.2.3}/docs/api/public-contract.md +0 -0
  40. {mt5cli-1.2.1 → mt5cli-1.2.3}/docs/api/schemas.md +0 -0
  41. {mt5cli-1.2.1 → mt5cli-1.2.3}/docs/api/telemetry.md +0 -0
  42. {mt5cli-1.2.1 → mt5cli-1.2.3}/docs/api/utils.md +0 -0
  43. {mt5cli-1.2.1 → mt5cli-1.2.3}/docs/index.md +0 -0
  44. {mt5cli-1.2.1 → mt5cli-1.2.3}/examples/grafana/README.md +0 -0
  45. {mt5cli-1.2.1 → mt5cli-1.2.3}/examples/grafana/compose.yml +0 -0
  46. {mt5cli-1.2.1 → mt5cli-1.2.3}/examples/grafana/dashboards/mt5cli-market.json +0 -0
  47. {mt5cli-1.2.1 → mt5cli-1.2.3}/examples/grafana/dashboards/mt5cli-overview.json +0 -0
  48. {mt5cli-1.2.1 → mt5cli-1.2.3}/examples/grafana/dashboards/mt5cli-trades.json +0 -0
  49. {mt5cli-1.2.1 → mt5cli-1.2.3}/examples/grafana/provisioning/dashboards/mt5cli.yml +0 -0
  50. {mt5cli-1.2.1 → mt5cli-1.2.3}/examples/grafana/provisioning/datasources/mt5cli-sqlite.yml +0 -0
  51. {mt5cli-1.2.1 → mt5cli-1.2.3}/mkdocs.yml +0 -0
  52. {mt5cli-1.2.1 → mt5cli-1.2.3}/mt5cli/__init__.py +0 -0
  53. {mt5cli-1.2.1 → mt5cli-1.2.3}/mt5cli/__main__.py +0 -0
  54. {mt5cli-1.2.1 → mt5cli-1.2.3}/mt5cli/contract.py +0 -0
  55. {mt5cli-1.2.1 → mt5cli-1.2.3}/mt5cli/converters.py +0 -0
  56. {mt5cli-1.2.1 → mt5cli-1.2.3}/mt5cli/exceptions.py +0 -0
  57. {mt5cli-1.2.1 → mt5cli-1.2.3}/mt5cli/grafana.py +0 -0
  58. {mt5cli-1.2.1 → mt5cli-1.2.3}/mt5cli/marketdata.py +0 -0
  59. {mt5cli-1.2.1 → mt5cli-1.2.3}/mt5cli/observability.py +0 -0
  60. {mt5cli-1.2.1 → mt5cli-1.2.3}/mt5cli/retry.py +0 -0
  61. {mt5cli-1.2.1 → mt5cli-1.2.3}/mt5cli/schemas.py +0 -0
  62. {mt5cli-1.2.1 → mt5cli-1.2.3}/mt5cli/telemetry.py +0 -0
  63. {mt5cli-1.2.1 → mt5cli-1.2.3}/mt5cli/utils.py +0 -0
  64. {mt5cli-1.2.1 → mt5cli-1.2.3}/tests/__init__.py +0 -0
  65. {mt5cli-1.2.1 → mt5cli-1.2.3}/tests/conftest.py +0 -0
  66. {mt5cli-1.2.1 → mt5cli-1.2.3}/tests/test_client.py +0 -0
  67. {mt5cli-1.2.1 → mt5cli-1.2.3}/tests/test_contracts.py +0 -0
  68. {mt5cli-1.2.1 → mt5cli-1.2.3}/tests/test_examples.py +0 -0
  69. {mt5cli-1.2.1 → mt5cli-1.2.3}/tests/test_grafana.py +0 -0
  70. {mt5cli-1.2.1 → mt5cli-1.2.3}/tests/test_marketdata.py +0 -0
  71. {mt5cli-1.2.1 → mt5cli-1.2.3}/tests/test_observability.py +0 -0
  72. {mt5cli-1.2.1 → mt5cli-1.2.3}/tests/test_packaging.py +0 -0
  73. {mt5cli-1.2.1 → mt5cli-1.2.3}/tests/test_telemetry.py +0 -0
  74. {mt5cli-1.2.1 → mt5cli-1.2.3}/tests/test_utils.py +0 -0
@@ -50,6 +50,21 @@ jobs:
50
50
  with:
51
51
  package-path: .
52
52
  runs-on: windows-latest
53
+ minimum-dependency-smoke-test:
54
+ if: >
55
+ github.event_name == 'push'
56
+ || github.event_name == 'pull_request'
57
+ || (github.event_name == 'workflow_dispatch' && inputs.workflow == 'lint-and-test')
58
+ permissions:
59
+ contents: read
60
+ runs-on: windows-latest
61
+ steps:
62
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.2
63
+ with:
64
+ persist-credentials: false
65
+ - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
66
+ - name: Check CLI help with minimum Click and Typer versions
67
+ run: uv run --with click==8.4.0 --with typer==0.26.0 mt5cli --help
53
68
  python-docs-deploy:
54
69
  if: >
55
70
  github.event_name == 'push'
@@ -26,7 +26,7 @@ Apply KISS, DRY, and YAGNI when changing code. Prefer the simplest implementatio
26
26
 
27
27
  ## Testing Guidelines
28
28
 
29
- Tests use pytest, pytest-mock, doctests, and pytest-cov. Test files should match `tests/test_*.py`, classes `Test*`, and functions `test_*`. Coverage is configured with `fail_under = 100`, so add focused tests for every behavior change. Mock MT5/pdmt5 boundaries; do not require a live MetaTrader terminal in unit tests.
29
+ Tests use pytest, pytest-mock, doctests, and pytest-cov. Test files should match `tests/test_*.py`, classes `Test*`, and functions `test_*`. Parametrize unit tests with `pytest.mark.parametrize` when the same behavior needs coverage across multiple inputs. Coverage is configured with `fail_under = 100`, so add focused tests for every behavior change. Mock MT5/pdmt5 boundaries; do not require a live MetaTrader terminal in unit tests.
30
30
 
31
31
  ## Commit & Pull Request Guidelines
32
32
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mt5cli
3
- Version: 1.2.1
3
+ Version: 1.2.3
4
4
  Summary: Generic MT5 data and execution infrastructure for Python applications
5
5
  Project-URL: Repository, https://github.com/dceoy/mt5cli.git
6
6
  Author-email: dceoy <dceoy@users.noreply.github.com>
@@ -16,10 +16,10 @@ Classifier: Programming Language :: Python
16
16
  Classifier: Programming Language :: Python :: 3
17
17
  Classifier: Topic :: Office/Business :: Financial :: Investment
18
18
  Requires-Python: <3.14,>=3.11
19
- Requires-Dist: click>=8.1.0
19
+ Requires-Dist: click>=8.4
20
20
  Requires-Dist: pandas>=2.2.2
21
21
  Requires-Dist: pdmt5>=1.2.0
22
- Requires-Dist: typer>=0.15.0
22
+ Requires-Dist: typer>=0.26.0
23
23
  Provides-Extra: otel
24
24
  Requires-Dist: opentelemetry-api; extra == 'otel'
25
25
  Requires-Dist: opentelemetry-exporter-otlp-proto-http; extra == 'otel'
@@ -202,19 +202,24 @@ or `None` result from the underlying client is normalized to an empty DataFrame.
202
202
  Downstream packages own all strategy-specific transformations. mt5cli does not
203
203
  provide entry-deal classification, Kelly sizing, or any betting-specific helpers.
204
204
 
205
- ## Broker server clock offset
205
+ ## Timestamp sources and broker clock offsets
206
206
 
207
- MT5 tick, bar, and deal timestamps are epoch values labeled in the broker
208
- server's wall clock, which is commonly UTC+2 or UTC+3 rather than true UTC.
209
- Any code that mixes those timestamps with true UTC (freshness checks,
210
- trailing history windows) silently inherits the broker's offset as a bias
211
- unless it is measured and applied explicitly.
207
+ The MT5 Python documentation describes tick and bar data copied by the
208
+ `copy_ticks_*` and `copy_rates_*` functions as UTC. It does not document a
209
+ different timezone contract for `symbol_info_tick()`, nor should deal epochs
210
+ be assumed to use a broker wall clock without source-specific evidence.
211
+
212
+ `get_tick_snapshot()` preserves the numeric MT5 epoch value in `time`; it does
213
+ not expose pdmt5's timezone-naive `Timestamp` conversion or alter the instant.
214
+ Any broker-specific offset handling therefore requires evidence from the live
215
+ data source in use and must be applied separately. Do not subtract a presumed
216
+ UTC+2 or UTC+3 offset from timestamps that are already UTC.
212
217
 
213
218
  `estimate_server_clock_offset_seconds()` reads the latest tick for a symbol
214
219
  and returns the broker's clock offset from true UTC, rounded to the nearest
215
- half hour, or `None` when no valid tick time is available. Apply the result in
216
- the downstream time-window calculation when comparing broker timestamps to
217
- true UTC.
220
+ half hour, or `None` when no valid tick time is available. Use it to validate a
221
+ suspected offset for the live `symbol_info_tick()` source before applying the
222
+ result in downstream time-window calculations.
218
223
 
219
224
  No offset is applied automatically by history retrieval helpers.
220
225
 
@@ -51,7 +51,7 @@ logger = logging.getLogger(__name__)
51
51
  # ---------------------------------------------------------------------------
52
52
 
53
53
 
54
- class _DateTimeType(click.ParamType):
54
+ class _DateTimeType(click.ParamType[datetime]):
55
55
  """Click parameter type for ISO 8601 datetime strings."""
56
56
 
57
57
  name = "DATETIME"
@@ -59,8 +59,8 @@ class _DateTimeType(click.ParamType):
59
59
  def convert(
60
60
  self,
61
61
  value: object,
62
- param: click.Parameter | None,
63
- ctx: click.Context | None,
62
+ param: click.Parameter | None = None,
63
+ ctx: click.Context | None = None,
64
64
  ) -> datetime:
65
65
  """Convert a string value to a timezone-aware datetime.
66
66
 
@@ -80,7 +80,7 @@ class _DateTimeType(click.ParamType):
80
80
  self.fail(str(exc), param, ctx)
81
81
 
82
82
 
83
- class _TimeframeType(click.ParamType):
83
+ class _TimeframeType(click.ParamType[int]):
84
84
  """Click parameter type for MT5 timeframe values."""
85
85
 
86
86
  name = "TIMEFRAME"
@@ -88,8 +88,8 @@ class _TimeframeType(click.ParamType):
88
88
  def convert(
89
89
  self,
90
90
  value: object,
91
- param: click.Parameter | None,
92
- ctx: click.Context | None,
91
+ param: click.Parameter | None = None,
92
+ ctx: click.Context | None = None,
93
93
  ) -> int:
94
94
  """Convert a string or integer value to a timeframe integer.
95
95
 
@@ -107,7 +107,7 @@ class _TimeframeType(click.ParamType):
107
107
  self.fail(str(exc), param, ctx)
108
108
 
109
109
 
110
- class _TickFlagsType(click.ParamType):
110
+ class _TickFlagsType(click.ParamType[int]):
111
111
  """Click parameter type for MT5 tick copy flags."""
112
112
 
113
113
  name = "FLAGS"
@@ -115,8 +115,8 @@ class _TickFlagsType(click.ParamType):
115
115
  def convert(
116
116
  self,
117
117
  value: object,
118
- param: click.Parameter | None,
119
- ctx: click.Context | None,
118
+ param: click.Parameter | None = None,
119
+ ctx: click.Context | None = None,
120
120
  ) -> int:
121
121
  """Convert a string or integer value to a tick flags integer.
122
122
 
@@ -134,7 +134,7 @@ class _TickFlagsType(click.ParamType):
134
134
  self.fail(str(exc), param, ctx)
135
135
 
136
136
 
137
- class _RequestType(click.ParamType):
137
+ class _RequestType(click.ParamType[dict[str, Any]]):
138
138
  """Click parameter type for JSON order requests."""
139
139
 
140
140
  name = "REQUEST"
@@ -142,8 +142,8 @@ class _RequestType(click.ParamType):
142
142
  def convert(
143
143
  self,
144
144
  value: object,
145
- param: click.Parameter | None,
146
- ctx: click.Context | None,
145
+ param: click.Parameter | None = None,
146
+ ctx: click.Context | None = None,
147
147
  ) -> dict[str, Any]:
148
148
  """Convert a raw CLI value to an order request dictionary.
149
149
 
@@ -166,6 +166,35 @@ TIMEFRAME_TYPE = _TimeframeType()
166
166
  TICK_FLAGS_TYPE = _TickFlagsType()
167
167
  REQUEST_TYPE = _RequestType()
168
168
 
169
+
170
+ def _parse_datetime_parameter(value: str) -> datetime:
171
+ try:
172
+ return parse_datetime(value)
173
+ except ValueError as exc:
174
+ raise typer.BadParameter(str(exc)) from exc
175
+
176
+
177
+ def _parse_timeframe_parameter(value: str) -> int:
178
+ try:
179
+ return parse_timeframe(value)
180
+ except ValueError as exc:
181
+ raise typer.BadParameter(str(exc)) from exc
182
+
183
+
184
+ def _parse_tick_flags_parameter(value: str) -> int:
185
+ try:
186
+ return parse_tick_flags(value)
187
+ except ValueError as exc:
188
+ raise typer.BadParameter(str(exc)) from exc
189
+
190
+
191
+ def _parse_request_parameter(value: str) -> dict[str, Any]:
192
+ try:
193
+ return parse_request(value)
194
+ except ValueError as exc:
195
+ raise typer.BadParameter(str(exc)) from exc
196
+
197
+
169
198
  # ---------------------------------------------------------------------------
170
199
  # Export context
171
200
  # ---------------------------------------------------------------------------
@@ -363,14 +392,14 @@ def rates_from(
363
392
  timeframe: Annotated[
364
393
  int,
365
394
  typer.Option(
366
- click_type=TIMEFRAME_TYPE,
395
+ parser=_parse_timeframe_parameter,
367
396
  help="Timeframe (e.g., M1, H1, D1, or integer).",
368
397
  ),
369
398
  ],
370
399
  date_from: Annotated[
371
400
  datetime,
372
401
  typer.Option(
373
- click_type=DATETIME_TYPE,
402
+ parser=_parse_datetime_parameter,
374
403
  help="Start date in ISO 8601 format.",
375
404
  ),
376
405
  ],
@@ -390,7 +419,7 @@ def rates_from_pos(
390
419
  timeframe: Annotated[
391
420
  int,
392
421
  typer.Option(
393
- click_type=TIMEFRAME_TYPE,
422
+ parser=_parse_timeframe_parameter,
394
423
  help="Timeframe.",
395
424
  ),
396
425
  ],
@@ -416,7 +445,7 @@ def latest_rates(
416
445
  timeframe: Annotated[
417
446
  int,
418
447
  typer.Option(
419
- click_type=TIMEFRAME_TYPE,
448
+ parser=_parse_timeframe_parameter,
420
449
  help="Timeframe.",
421
450
  ),
422
451
  ],
@@ -445,17 +474,17 @@ def rates_range(
445
474
  timeframe: Annotated[
446
475
  int,
447
476
  typer.Option(
448
- click_type=TIMEFRAME_TYPE,
477
+ parser=_parse_timeframe_parameter,
449
478
  help="Timeframe.",
450
479
  ),
451
480
  ],
452
481
  date_from: Annotated[
453
482
  datetime,
454
- typer.Option(click_type=DATETIME_TYPE, help="Start date."),
483
+ typer.Option(parser=_parse_datetime_parameter, help="Start date."),
455
484
  ],
456
485
  date_to: Annotated[
457
486
  datetime,
458
- typer.Option(click_type=DATETIME_TYPE, help="End date."),
487
+ typer.Option(parser=_parse_datetime_parameter, help="End date."),
459
488
  ],
460
489
  ) -> None:
461
490
  """Export rates for a date range."""
@@ -471,13 +500,13 @@ def ticks_from(
471
500
  symbol: Annotated[str, typer.Option(help="Symbol name.")],
472
501
  date_from: Annotated[
473
502
  datetime,
474
- typer.Option(click_type=DATETIME_TYPE, help="Start date."),
503
+ typer.Option(parser=_parse_datetime_parameter, help="Start date."),
475
504
  ],
476
505
  count: Annotated[int, typer.Option(help="Number of ticks.")],
477
506
  flags: Annotated[
478
507
  int,
479
508
  typer.Option(
480
- click_type=TICK_FLAGS_TYPE,
509
+ parser=_parse_tick_flags_parameter,
481
510
  help="Tick flags (ALL, INFO, TRADE, or integer).",
482
511
  ),
483
512
  ],
@@ -495,15 +524,15 @@ def ticks_range(
495
524
  symbol: Annotated[str, typer.Option(help="Symbol name.")],
496
525
  date_from: Annotated[
497
526
  datetime,
498
- typer.Option(click_type=DATETIME_TYPE, help="Start date."),
527
+ typer.Option(parser=_parse_datetime_parameter, help="Start date."),
499
528
  ],
500
529
  date_to: Annotated[
501
530
  datetime,
502
- typer.Option(click_type=DATETIME_TYPE, help="End date."),
531
+ typer.Option(parser=_parse_datetime_parameter, help="End date."),
503
532
  ],
504
533
  flags: Annotated[
505
534
  int,
506
- typer.Option(click_type=TICK_FLAGS_TYPE, help="Tick flags."),
535
+ typer.Option(parser=_parse_tick_flags_parameter, help="Tick flags."),
507
536
  ],
508
537
  ) -> None:
509
538
  """Export ticks for a date range."""
@@ -523,7 +552,7 @@ def ticks_recent(
523
552
  ],
524
553
  date_to: Annotated[
525
554
  datetime | None,
526
- typer.Option(click_type=DATETIME_TYPE, help="Window end date."),
555
+ typer.Option(parser=_parse_datetime_parameter, help="Window end date."),
527
556
  ] = None,
528
557
  count: Annotated[
529
558
  int,
@@ -532,7 +561,7 @@ def ticks_recent(
532
561
  flags: Annotated[
533
562
  int,
534
563
  typer.Option(
535
- click_type=TICK_FLAGS_TYPE,
564
+ parser=_parse_tick_flags_parameter,
536
565
  help="Tick flags (ALL, INFO, TRADE, or integer).",
537
566
  ),
538
567
  ] = "ALL", # pyright: ignore[reportArgumentType]
@@ -625,11 +654,11 @@ def history_orders(
625
654
  ctx: typer.Context,
626
655
  date_from: Annotated[
627
656
  datetime | None,
628
- typer.Option(click_type=DATETIME_TYPE, help="Start date."),
657
+ typer.Option(parser=_parse_datetime_parameter, help="Start date."),
629
658
  ] = None,
630
659
  date_to: Annotated[
631
660
  datetime | None,
632
- typer.Option(click_type=DATETIME_TYPE, help="End date."),
661
+ typer.Option(parser=_parse_datetime_parameter, help="End date."),
633
662
  ] = None,
634
663
  group: Annotated[str | None, typer.Option(help="Group filter.")] = None,
635
664
  symbol: Annotated[str | None, typer.Option(help="Symbol filter.")] = None,
@@ -655,11 +684,11 @@ def history_deals(
655
684
  ctx: typer.Context,
656
685
  date_from: Annotated[
657
686
  datetime | None,
658
- typer.Option(click_type=DATETIME_TYPE, help="Start date."),
687
+ typer.Option(parser=_parse_datetime_parameter, help="Start date."),
659
688
  ] = None,
660
689
  date_to: Annotated[
661
690
  datetime | None,
662
- typer.Option(click_type=DATETIME_TYPE, help="End date."),
691
+ typer.Option(parser=_parse_datetime_parameter, help="End date."),
663
692
  ] = None,
664
693
  group: Annotated[str | None, typer.Option(help="Group filter.")] = None,
665
694
  symbol: Annotated[str | None, typer.Option(help="Symbol filter.")] = None,
@@ -686,7 +715,7 @@ def recent_history_deals(
686
715
  hours: Annotated[float, typer.Option(help="Lookback window in hours.")],
687
716
  date_to: Annotated[
688
717
  datetime | None,
689
- typer.Option(click_type=DATETIME_TYPE, help="Window end date."),
718
+ typer.Option(parser=_parse_datetime_parameter, help="Window end date."),
690
719
  ] = None,
691
720
  group: Annotated[str | None, typer.Option(help="Group filter.")] = None,
692
721
  symbol: Annotated[str | None, typer.Option(help="Symbol filter.")] = None,
@@ -744,7 +773,7 @@ def order_check(
744
773
  ctx: typer.Context,
745
774
  request: Annotated[
746
775
  dict[str, Any],
747
- typer.Option(click_type=REQUEST_TYPE, help=_REQUEST_OPTION_HELP),
776
+ typer.Option(parser=_parse_request_parameter, help=_REQUEST_OPTION_HELP),
748
777
  ],
749
778
  ) -> None:
750
779
  """Check funds sufficiency for a trading operation."""
@@ -756,7 +785,7 @@ def order_send(
756
785
  ctx: typer.Context,
757
786
  request: Annotated[
758
787
  dict[str, Any],
759
- typer.Option(click_type=REQUEST_TYPE, help=_REQUEST_OPTION_HELP),
788
+ typer.Option(parser=_parse_request_parameter, help=_REQUEST_OPTION_HELP),
760
789
  ],
761
790
  yes: Annotated[
762
791
  bool,
@@ -995,11 +1024,11 @@ def collect_history(
995
1024
  ],
996
1025
  date_from: Annotated[
997
1026
  datetime,
998
- typer.Option(click_type=DATETIME_TYPE, help="Start date."),
1027
+ typer.Option(parser=_parse_datetime_parameter, help="Start date."),
999
1028
  ],
1000
1029
  date_to: Annotated[
1001
1030
  datetime,
1002
- typer.Option(click_type=DATETIME_TYPE, help="End date."),
1031
+ typer.Option(parser=_parse_datetime_parameter, help="End date."),
1003
1032
  ],
1004
1033
  dataset: Annotated[
1005
1034
  list[Dataset] | None,
@@ -1016,14 +1045,14 @@ def collect_history(
1016
1045
  timeframe: Annotated[
1017
1046
  int,
1018
1047
  typer.Option(
1019
- click_type=TIMEFRAME_TYPE,
1048
+ parser=_parse_timeframe_parameter,
1020
1049
  help="Rates timeframe (e.g., M1, H1, D1).",
1021
1050
  ),
1022
1051
  ] = 1,
1023
1052
  flags: Annotated[
1024
1053
  int,
1025
1054
  typer.Option(
1026
- click_type=TICK_FLAGS_TYPE,
1055
+ parser=_parse_tick_flags_parameter,
1027
1056
  help="Tick copy flags (ALL, INFO, TRADE, or integer).",
1028
1057
  ),
1029
1058
  ] = "ALL", # pyright: ignore[reportArgumentType]
@@ -787,6 +787,31 @@ class _BaseMT5Client:
787
787
  """Return the last tick for a symbol."""
788
788
  return self._fetch(lambda c: c.symbol_info_tick_as_df(symbol=symbol))
789
789
 
790
+ def symbol_info_tick_as_dict(
791
+ self,
792
+ symbol: str,
793
+ *,
794
+ skip_to_datetime: bool = False,
795
+ ) -> dict[str, object]:
796
+ """Return the last tick as a plain mapping.
797
+
798
+ Args:
799
+ symbol: Symbol name.
800
+ skip_to_datetime: Preserve numeric MT5 time fields when ``True``.
801
+
802
+ Returns:
803
+ Tick fields using the underlying pdmt5 conversion behavior.
804
+ """
805
+ return self._fetch_value(
806
+ lambda c: cast(
807
+ "dict[str, object]",
808
+ c.symbol_info_tick_as_dict(
809
+ symbol=symbol,
810
+ skip_to_datetime=skip_to_datetime,
811
+ ),
812
+ )
813
+ )
814
+
790
815
  def market_book(self, symbol: str) -> pd.DataFrame:
791
816
  """Return market depth for a symbol."""
792
817
  return self._fetch(lambda c: c.market_book_get_as_df(symbol=symbol))
@@ -1749,9 +1749,17 @@ def _stream_symbol_frames(
1749
1749
  """
1750
1750
  table_exists = False
1751
1751
  for sym in symbols:
1752
+ frame = fetch_frame(sym)
1753
+ if len(frame.columns) == 0:
1754
+ logger.warning(
1755
+ "Skipping %s for symbol=%s: dataset returned no columns",
1756
+ dataset.table_name,
1757
+ sym,
1758
+ )
1759
+ continue
1752
1760
  table_exists = write_streamed_frame(
1753
1761
  conn,
1754
- fetch_frame(sym),
1762
+ frame,
1755
1763
  dataset,
1756
1764
  table_exists,
1757
1765
  if_exists,
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  import logging
6
6
  from dataclasses import asdict, dataclass
7
7
  from datetime import UTC, datetime
8
+ from inspect import signature
8
9
  from math import floor, isfinite
9
10
  from numbers import Integral, Real
10
11
  from typing import TYPE_CHECKING, Literal, Protocol, TypedDict, cast
@@ -20,6 +21,7 @@ if TYPE_CHECKING:
20
21
  from typing import Any
21
22
 
22
23
  _logger = logging.getLogger(__name__)
24
+ _DATETIME_TYPES = (datetime, pd.Timestamp)
23
25
 
24
26
 
25
27
  class _Mt5ClientProtocol(Protocol):
@@ -801,16 +803,57 @@ def get_tick_snapshot(
801
803
  client: _Mt5ClientProtocol,
802
804
  symbol: str,
803
805
  ) -> dict[str, float | int | None]:
804
- """Return normalized latest tick data, including bid, ask, and timestamp."""
806
+ """Return normalized latest tick data with a numeric MT5 timestamp.
807
+
808
+ The numeric ``time`` preserves the original value returned by MT5 without
809
+ applying timezone or server-offset correction.
810
+ """
805
811
  method = getattr(client, "symbol_info_tick_as_dict", None)
806
- value = (
807
- method(symbol=symbol) if callable(method) else client.symbol_info_tick(symbol)
808
- )
812
+ if callable(method):
813
+ try:
814
+ signature(method).bind(symbol=symbol, skip_to_datetime=True)
815
+ except (TypeError, ValueError):
816
+ value = method(symbol=symbol)
817
+ else:
818
+ value = method(symbol=symbol, skip_to_datetime=True)
819
+ else:
820
+ value = client.symbol_info_tick(symbol)
809
821
  snapshot = _snapshot_from_value(value, _TICK_SNAPSHOT_FIELDS)
810
822
  snapshot["symbol"] = snapshot.get("symbol") or symbol
823
+ snapshot["time"] = _numeric_tick_time(snapshot.get("time"))
811
824
  return cast("dict[str, float | int | None]", snapshot)
812
825
 
813
826
 
827
+ def _numeric_tick_time(value: object) -> float | int | None:
828
+ """Normalize an MT5 timestamp to its numeric epoch representation.
829
+
830
+ Returns:
831
+ The numeric timestamp, or ``None`` for an unsupported value.
832
+ """
833
+ numeric: float | None
834
+ if value is None or isinstance(value, bool):
835
+ numeric = None
836
+ elif isinstance(value, _DATETIME_TYPES):
837
+ if pd.isna(value):
838
+ return None
839
+ timestamp = pd.Timestamp(value)
840
+ if timestamp.tzinfo is None:
841
+ timestamp = timestamp.tz_localize(UTC)
842
+ numeric = timestamp.timestamp()
843
+ elif isinstance(value, Integral):
844
+ return int(value)
845
+ elif isinstance(value, Real):
846
+ numeric = float(value)
847
+ elif isinstance(value, str):
848
+ try:
849
+ numeric = float(value)
850
+ except ValueError:
851
+ return None
852
+ else:
853
+ numeric = None
854
+ return numeric if numeric is not None and isfinite(numeric) else None
855
+
856
+
814
857
  _SERVER_CLOCK_OFFSET_ROUNDING_SECONDS = 1800.0
815
858
  _MAX_PLAUSIBLE_SERVER_CLOCK_OFFSET_SECONDS = 14 * 3600.0
816
859
 
@@ -821,12 +864,12 @@ def estimate_server_clock_offset_seconds(
821
864
  ) -> float | None:
822
865
  """Estimate the broker server clock offset from true UTC.
823
866
 
824
- MT5 tick, bar, and deal timestamps are epoch values labeled in the
825
- broker server's wall clock, which is commonly UTC+2 or UTC+3 rather
826
- than true UTC. This reads the latest tick for ``symbol`` and compares
827
- its timestamp to the current UTC time, rounding to the nearest half
828
- hour: server offsets are whole or half-hour multiples, and rounding
829
- absorbs a few minutes of real tick staleness.
867
+ MT5 documents copied tick and bar data as UTC, while the timezone contract
868
+ for ``symbol_info_tick()`` is not explicit. This helper reads the latest
869
+ tick for ``symbol`` to validate a suspected broker-specific clock offset
870
+ before downstream correction. It compares the tick timestamp to the
871
+ current UTC time and rounds to the nearest half hour, absorbing a few
872
+ minutes of real tick staleness.
830
873
 
831
874
  On a closed market, weekend, holiday, or illiquid symbol the latest tick
832
875
  can be hours or days old, in which case the tick-vs-now delta reflects
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mt5cli"
3
- version = "1.2.1"
3
+ version = "1.2.3"
4
4
  description = "Generic MT5 data and execution infrastructure for Python applications"
5
5
  authors = [{name = "dceoy", email = "dceoy@users.noreply.github.com"}]
6
6
  maintainers = [{name = "dceoy", email = "dceoy@users.noreply.github.com"}]
@@ -11,8 +11,8 @@ requires-python = ">= 3.11, < 3.14"
11
11
  dependencies = [
12
12
  "pdmt5 >= 1.2.0",
13
13
  "pandas >= 2.2.2",
14
- "click >= 8.1.0",
15
- "typer >= 0.15.0",
14
+ "click >= 8.4",
15
+ "typer >= 0.26.0",
16
16
  ]
17
17
  classifiers = [
18
18
  "Development Status :: 3 - Alpha",
@@ -25,6 +25,10 @@ from mt5cli.cli import (
25
25
  TIMEFRAME_TYPE, # type: ignore[reportPrivateUsage]
26
26
  _execute_export, # type: ignore[reportPrivateUsage]
27
27
  _ExportContext, # type: ignore[reportPrivateUsage]
28
+ _parse_datetime_parameter, # type: ignore[reportPrivateUsage]
29
+ _parse_request_parameter, # type: ignore[reportPrivateUsage]
30
+ _parse_tick_flags_parameter, # type: ignore[reportPrivateUsage]
31
+ _parse_timeframe_parameter, # type: ignore[reportPrivateUsage]
28
32
  _sdk_client, # type: ignore[reportPrivateUsage]
29
33
  app,
30
34
  main,
@@ -2195,6 +2199,11 @@ class TestDateTimeType:
2195
2199
  with pytest.raises(Exception, match="Invalid datetime"):
2196
2200
  DATETIME_TYPE.convert("bad", None, None)
2197
2201
 
2202
+ def test_parser_invalid(self) -> None:
2203
+ """Test that Typer receives a BadParameter with validation details."""
2204
+ with pytest.raises(Exception, match="Invalid datetime"):
2205
+ _parse_datetime_parameter("bad")
2206
+
2198
2207
 
2199
2208
  class TestTimeframeType:
2200
2209
  """Tests for _TimeframeType."""
@@ -2224,6 +2233,11 @@ class TestTimeframeType:
2224
2233
  with pytest.raises(Exception, match="Invalid timeframe"):
2225
2234
  TIMEFRAME_TYPE.convert(value, None, None)
2226
2235
 
2236
+ def test_parser_invalid(self) -> None:
2237
+ """Test that Typer receives a BadParameter with validation details."""
2238
+ with pytest.raises(Exception, match="Invalid timeframe"):
2239
+ _parse_timeframe_parameter("bad")
2240
+
2227
2241
 
2228
2242
  class TestTickFlagsType:
2229
2243
  """Tests for _TickFlagsType."""
@@ -2253,6 +2267,11 @@ class TestTickFlagsType:
2253
2267
  with pytest.raises(Exception, match="Invalid tick flags"):
2254
2268
  TICK_FLAGS_TYPE.convert(value, None, None)
2255
2269
 
2270
+ def test_parser_invalid(self) -> None:
2271
+ """Test that Typer receives a BadParameter with validation details."""
2272
+ with pytest.raises(Exception, match="Invalid tick flags"):
2273
+ _parse_tick_flags_parameter("bad")
2274
+
2256
2275
 
2257
2276
  class TestRequestType:
2258
2277
  """Tests for _RequestType."""
@@ -2265,3 +2284,8 @@ class TestRequestType:
2265
2284
  """Test that invalid values raise BadParameter."""
2266
2285
  with pytest.raises(Exception, match="Invalid JSON request"):
2267
2286
  REQUEST_TYPE.convert("bad", None, None)
2287
+
2288
+ def test_parser_invalid(self) -> None:
2289
+ """Test that Typer receives a BadParameter with validation details."""
2290
+ with pytest.raises(Exception, match="Invalid JSON request"):
2291
+ _parse_request_parameter("bad")
@@ -2388,7 +2388,10 @@ class TestIncrementalIntegration:
2388
2388
  "SELECT ticket, time_setup, type FROM history_orders",
2389
2389
  ).fetchall()
2390
2390
  assert rows == [(1, "2024-01-01T00:00:01+00:00", 1)]
2391
- assert "Skipping history_orders: dataset returned no columns" in caplog.text
2391
+ assert (
2392
+ "Skipping history_orders for symbol=GBPUSD: dataset returned no columns"
2393
+ in caplog.text
2394
+ )
2392
2395
 
2393
2396
  def test_write_collected_datasets_and_edge_branches(
2394
2397
  self,
@@ -2541,12 +2544,16 @@ class TestIncrementalIntegration:
2541
2544
  def test_write_rates_skips_empty_schema(
2542
2545
  self,
2543
2546
  tmp_path: Path,
2547
+ caplog: pytest.LogCaptureFixture,
2544
2548
  ) -> None:
2545
- """Test rates writer skips frames with no columns after normalization."""
2549
+ """Test rates writer logs the symbol when a frame has no columns."""
2546
2550
  client = MagicMock()
2547
2551
  client.copy_rates_range.return_value = pd.DataFrame()
2548
2552
  written_columns: dict[Dataset, set[str]] = {}
2549
- with sqlite3.connect(tmp_path / "empty-rates.db") as conn:
2553
+ with (
2554
+ caplog.at_level(logging.WARNING, logger="mt5cli.history"),
2555
+ sqlite3.connect(tmp_path / "empty-rates.db") as conn,
2556
+ ):
2550
2557
  assert not write_rates_dataset(
2551
2558
  conn,
2552
2559
  client,
@@ -2557,6 +2564,10 @@ class TestIncrementalIntegration:
2557
2564
  IfExists.APPEND,
2558
2565
  written_columns,
2559
2566
  )
2567
+ assert (
2568
+ "Skipping rates for symbol=EURUSD: dataset returned no columns"
2569
+ in caplog.text
2570
+ )
2560
2571
 
2561
2572
  def test_write_symbols_dataset_snapshots_metadata(
2562
2573
  self,
@@ -15,6 +15,7 @@ from pdmt5 import Mt5RuntimeError
15
15
  from pytest_mock import MockerFixture # noqa: TC002
16
16
 
17
17
  from mt5cli import trading
18
+ from mt5cli.client import MT5Client
18
19
  from mt5cli.exceptions import Mt5OperationError
19
20
  from mt5cli.trading import (
20
21
  MarginVolume,
@@ -671,6 +672,99 @@ class TestSnapshotsAndState:
671
672
  assert get_symbol_snapshot(client, "EURUSD")["symbol"] == "EURUSD"
672
673
  assert get_tick_snapshot(client, "EURUSD")["symbol"] == "EURUSD"
673
674
 
675
+ def test_tick_snapshot_requests_raw_time_from_mt5_client(self) -> None:
676
+ """Test the canonical client preserves pdmt5's numeric MT5 time."""
677
+
678
+ class ConnectedClient:
679
+ def symbol_info_tick_as_dict(
680
+ self,
681
+ symbol: str,
682
+ skip_to_datetime: bool = False,
683
+ ) -> dict[str, float]:
684
+ assert symbol == "USOIL"
685
+ assert skip_to_datetime is True
686
+ return {"time": 1717250400, "bid": 78.5, "ask": 78.6}
687
+
688
+ client = MT5Client.from_connected_client(ConnectedClient())
689
+
690
+ snapshot = get_tick_snapshot(client, "USOIL")
691
+
692
+ assert snapshot["time"] == 1717250400
693
+ _assert_close(snapshot["bid"], 78.5)
694
+ _assert_close(snapshot["ask"], 78.6)
695
+
696
+ def test_tick_snapshot_supports_dict_method_without_skip_keyword(self) -> None:
697
+ """Test compatible dictionary clients need not accept pdmt5 options."""
698
+
699
+ class DictionaryClient:
700
+ def symbol_info_tick_as_dict(self, *, symbol: str) -> dict[str, float]:
701
+ assert symbol == "UKOIL"
702
+ return {"time": 1717254000, "bid": 80.1, "ask": 80.2}
703
+
704
+ snapshot = get_tick_snapshot(
705
+ cast("_Mt5ClientProtocol", DictionaryClient()),
706
+ "UKOIL",
707
+ )
708
+
709
+ assert snapshot["time"] == 1717254000
710
+
711
+ @pytest.mark.parametrize(
712
+ "tick_time",
713
+ [
714
+ pd.Timestamp("2024-06-01 15:00:00"),
715
+ pd.Timestamp("2024-06-01 15:00:00", tz=UTC),
716
+ ],
717
+ ids=["naive", "timezone-aware"],
718
+ )
719
+ def test_tick_snapshot_normalizes_dataframe_timestamp_to_numeric(
720
+ self,
721
+ tick_time: pd.Timestamp,
722
+ ) -> None:
723
+ """Test DataFrame fallbacks cannot leak a naive pandas Timestamp."""
724
+ client = MagicMock()
725
+ del client.symbol_info_tick_as_dict
726
+ client.symbol_info_tick.return_value = pd.DataFrame(
727
+ [{"time": tick_time, "bid": 1.0}],
728
+ )
729
+
730
+ snapshot = get_tick_snapshot(client, "EURUSD")
731
+
732
+ _assert_close(snapshot["time"], 1717254000.0)
733
+ assert not isinstance(snapshot["time"], pd.Timestamp)
734
+
735
+ @pytest.mark.parametrize(
736
+ ("tick_time", "expected"),
737
+ [
738
+ (True, None),
739
+ (float("inf"), None),
740
+ (pd.NaT, None),
741
+ ("1717254000", 1717254000.0),
742
+ (object(), None),
743
+ ],
744
+ ids=[
745
+ "boolean",
746
+ "non-finite",
747
+ "missing-datetime",
748
+ "numeric-string",
749
+ "unsupported",
750
+ ],
751
+ )
752
+ def test_tick_snapshot_time_always_matches_numeric_contract(
753
+ self,
754
+ tick_time: object,
755
+ expected: float | None,
756
+ ) -> None:
757
+ """Test non-canonical time payloads normalize to a number or None."""
758
+ client = MagicMock()
759
+ client.symbol_info_tick_as_dict.return_value = {"time": tick_time}
760
+
761
+ snapshot = get_tick_snapshot(client, "EURUSD")
762
+
763
+ if expected is None:
764
+ assert snapshot["time"] is None
765
+ else:
766
+ _assert_close(snapshot["time"], expected)
767
+
674
768
  def test_symbol_and_tick_snapshots_use_object_fallbacks(self) -> None:
675
769
  """Test symbol and tick snapshots support non-dict MT5 values."""
676
770
  client = MagicMock()
@@ -3983,19 +4077,21 @@ class TestEstimateServerClockOffsetSeconds:
3983
4077
  client.symbol_info_tick_as_dict.return_value = {"time": tick_time}
3984
4078
  return client
3985
4079
 
3986
- def test_rounds_positive_offset_to_nearest_half_hour(
4080
+ @pytest.mark.parametrize("server_offset", [7200.0, 10800.0])
4081
+ def test_rounds_positive_broker_offset_to_nearest_half_hour(
3987
4082
  self,
3988
4083
  mocker: MockerFixture,
4084
+ server_offset: float,
3989
4085
  ) -> None:
3990
- """A tick ~3h ahead of UTC now estimates a rounded +3h offset."""
4086
+ """UTC+2 and UTC+3 broker tick labels yield their server offsets."""
3991
4087
  frozen = datetime(2024, 6, 1, 12, 0, 0, tzinfo=UTC)
3992
4088
  mock_dt = mocker.patch("mt5cli.trading.datetime")
3993
4089
  mock_dt.now.return_value = frozen
3994
- client = self._client_with_tick_time(frozen.timestamp() + 10800.4)
4090
+ client = self._client_with_tick_time(frozen.timestamp() + server_offset + 0.4)
3995
4091
 
3996
4092
  offset = estimate_server_clock_offset_seconds(client, "EURUSD")
3997
4093
 
3998
- _assert_close(offset, 10800.0)
4094
+ _assert_close(offset, server_offset)
3999
4095
 
4000
4096
  def test_stale_tick_on_utc_server_rounds_to_zero(
4001
4097
  self,
@@ -118,14 +118,14 @@ wheels = [
118
118
 
119
119
  [[package]]
120
120
  name = "click"
121
- version = "8.3.1"
121
+ version = "8.4.2"
122
122
  source = { registry = "https://pypi.org/simple" }
123
123
  dependencies = [
124
124
  { name = "colorama", marker = "sys_platform == 'win32'" },
125
125
  ]
126
- sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" }
126
+ sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" }
127
127
  wheels = [
128
- { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" },
128
+ { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" },
129
129
  ]
130
130
 
131
131
  [[package]]
@@ -499,7 +499,7 @@ wheels = [
499
499
 
500
500
  [[package]]
501
501
  name = "mt5cli"
502
- version = "1.2.1"
502
+ version = "1.2.3"
503
503
  source = { editable = "." }
504
504
  dependencies = [
505
505
  { name = "click" },
@@ -537,14 +537,14 @@ dev = [
537
537
 
538
538
  [package.metadata]
539
539
  requires-dist = [
540
- { name = "click", specifier = ">=8.1.0" },
540
+ { name = "click", specifier = ">=8.4" },
541
541
  { name = "opentelemetry-api", marker = "extra == 'otel'" },
542
542
  { name = "opentelemetry-exporter-otlp-proto-http", marker = "extra == 'otel'" },
543
543
  { name = "opentelemetry-sdk", marker = "extra == 'otel'" },
544
544
  { name = "pandas", specifier = ">=2.2.2" },
545
545
  { name = "pdmt5", specifier = ">=1.2.0" },
546
546
  { name = "pyarrow", marker = "extra == 'parquet'", specifier = ">=19.0.0" },
547
- { name = "typer", specifier = ">=0.15.0" },
547
+ { name = "typer", specifier = ">=0.26.0" },
548
548
  ]
549
549
  provides-extras = ["parquet", "otel"]
550
550
 
@@ -1213,17 +1213,17 @@ wheels = [
1213
1213
 
1214
1214
  [[package]]
1215
1215
  name = "typer"
1216
- version = "0.24.1"
1216
+ version = "0.26.8"
1217
1217
  source = { registry = "https://pypi.org/simple" }
1218
1218
  dependencies = [
1219
1219
  { name = "annotated-doc" },
1220
- { name = "click" },
1220
+ { name = "colorama", marker = "sys_platform == 'win32'" },
1221
1221
  { name = "rich" },
1222
1222
  { name = "shellingham" },
1223
1223
  ]
1224
- sdist = { url = "https://files.pythonhosted.org/packages/f5/24/cb09efec5cc954f7f9b930bf8279447d24618bb6758d4f6adf2574c41780/typer-0.24.1.tar.gz", hash = "sha256:e39b4732d65fbdcde189ae76cf7cd48aeae72919dea1fdfc16593be016256b45", size = 118613, upload-time = "2026-02-21T16:54:40.609Z" }
1224
+ sdist = { url = "https://files.pythonhosted.org/packages/7c/f7/68adc395201b20b872d68e975386832e8005ffeacedd43a1d837a32815be/typer-0.26.8.tar.gz", hash = "sha256:c244a6bd558886fe3f8780efb6bdd28bb9aff005a94eedebaa5cb32926fe2f7e", size = 202097, upload-time = "2026-06-26T09:22:45.705Z" }
1225
1225
  wheels = [
1226
- { url = "https://files.pythonhosted.org/packages/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-py3-none-any.whl", hash = "sha256:112c1f0ce578bfb4cab9ffdabc68f031416ebcc216536611ba21f04e9aa84c9e", size = 56085, upload-time = "2026-02-21T16:54:41.616Z" },
1226
+ { url = "https://files.pythonhosted.org/packages/80/87/b9fd69c92c6102a066e1b86a35243f53e70bd4c709f2a26d9f4fee4f4dc0/typer-0.26.8-py3-none-any.whl", hash = "sha256:3512ca79ac5c11113414b36e80281b872884477722440691c89d1112e321a49c", size = 122564, upload-time = "2026-06-26T09:22:44.72Z" },
1227
1227
  ]
1228
1228
 
1229
1229
  [[package]]
@@ -1,72 +0,0 @@
1
- ---
2
- name: Pull request review and mention bot using OpenCode
3
- on:
4
- pull_request:
5
- types:
6
- - opened
7
- - ready_for_review
8
- issue_comment:
9
- types:
10
- - created
11
- pull_request_review_comment:
12
- types:
13
- - created
14
- pull_request_review:
15
- types:
16
- - submitted
17
- issues:
18
- types:
19
- - opened
20
- - assigned
21
- permissions:
22
- contents: read
23
- jobs:
24
- opencode-review:
25
- if: >
26
- github.event_name == 'pull_request'
27
- && contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.pull_request.author_association)
28
- && (! github.event.pull_request.draft)
29
- && (! startsWith(github.head_ref, 'dependabot/'))
30
- && (! startsWith(github.head_ref, 'renovate/'))
31
- permissions:
32
- contents: read
33
- pull-requests: write
34
- issues: write
35
- id-token: write
36
- actions: read
37
- uses: dceoy/gh-actions-for-devops/.github/workflows/opencode-review.yml@main # zizmor: ignore[unpinned-uses]
38
- with:
39
- model: opencode-go/kimi-k2.7-code
40
- secrets:
41
- OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
42
- GH_TOKEN: ${{ secrets.GH_TOKEN || secrets.GITHUB_TOKEN }}
43
- opencode-bot:
44
- if: >
45
- (
46
- (github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment')
47
- && contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association)
48
- && (contains(github.event.comment.body, '/oc') || contains(github.event.comment.body, '/opencode'))
49
- ) || (
50
- github.event_name == 'pull_request_review'
51
- && contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.review.author_association)
52
- && (contains(github.event.review.body, '/oc') || contains(github.event.review.body, '/opencode'))
53
- ) || (
54
- github.event_name == 'issues'
55
- && contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.issue.author_association)
56
- && (
57
- (contains(github.event.issue.body, '/oc') || contains(github.event.issue.title, '/oc'))
58
- || (contains(github.event.issue.body, '/opencode') || contains(github.event.issue.title, '/opencode'))
59
- )
60
- )
61
- permissions:
62
- contents: read
63
- pull-requests: write
64
- issues: write
65
- id-token: write
66
- actions: read
67
- uses: dceoy/gh-actions-for-devops/.github/workflows/opencode-bot.yml@main # zizmor: ignore[unpinned-uses]
68
- with:
69
- model: opencode-go/glm-5.2
70
- secrets:
71
- OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
72
- GH_TOKEN: ${{ secrets.GH_TOKEN || secrets.GITHUB_TOKEN }}
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes