mt5cli 0.5.2__tar.gz → 0.6.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.
Files changed (38) hide show
  1. {mt5cli-0.5.2 → mt5cli-0.6.0}/PKG-INFO +19 -2
  2. {mt5cli-0.5.2 → mt5cli-0.6.0}/README.md +18 -1
  3. {mt5cli-0.5.2 → mt5cli-0.6.0}/docs/api/history.md +12 -0
  4. mt5cli-0.6.0/docs/api/sdk.md +103 -0
  5. {mt5cli-0.5.2 → mt5cli-0.6.0}/mt5cli/__init__.py +18 -0
  6. {mt5cli-0.5.2 → mt5cli-0.6.0}/mt5cli/history.py +66 -0
  7. {mt5cli-0.5.2 → mt5cli-0.6.0}/mt5cli/sdk.py +442 -2
  8. {mt5cli-0.5.2 → mt5cli-0.6.0}/pyproject.toml +1 -1
  9. {mt5cli-0.5.2 → mt5cli-0.6.0}/tests/test_history.py +92 -0
  10. {mt5cli-0.5.2 → mt5cli-0.6.0}/tests/test_sdk.py +507 -0
  11. {mt5cli-0.5.2 → mt5cli-0.6.0}/uv.lock +4 -4
  12. mt5cli-0.5.2/docs/api/sdk.md +0 -3
  13. {mt5cli-0.5.2 → mt5cli-0.6.0}/.agents/skills/local-qa/SKILL.md +0 -0
  14. {mt5cli-0.5.2 → mt5cli-0.6.0}/.agents/skills/local-qa/scripts/qa.sh +0 -0
  15. {mt5cli-0.5.2 → mt5cli-0.6.0}/.agents/skills/mt5cli/SKILL.md +0 -0
  16. {mt5cli-0.5.2 → mt5cli-0.6.0}/.claude/agents/codex.md +0 -0
  17. {mt5cli-0.5.2 → mt5cli-0.6.0}/.claude/settings.json +0 -0
  18. {mt5cli-0.5.2 → mt5cli-0.6.0}/.github/FUNDING.yml +0 -0
  19. {mt5cli-0.5.2 → mt5cli-0.6.0}/.github/dependabot.yml +0 -0
  20. {mt5cli-0.5.2 → mt5cli-0.6.0}/.github/renovate.json +0 -0
  21. {mt5cli-0.5.2 → mt5cli-0.6.0}/.github/workflows/ci.yml +0 -0
  22. {mt5cli-0.5.2 → mt5cli-0.6.0}/.github/workflows/claude.yml +0 -0
  23. {mt5cli-0.5.2 → mt5cli-0.6.0}/.github/workflows/release.yml +0 -0
  24. {mt5cli-0.5.2 → mt5cli-0.6.0}/.gitignore +0 -0
  25. {mt5cli-0.5.2 → mt5cli-0.6.0}/AGENTS.md +0 -0
  26. {mt5cli-0.5.2 → mt5cli-0.6.0}/CLAUDE.md +0 -0
  27. {mt5cli-0.5.2 → mt5cli-0.6.0}/LICENSE +0 -0
  28. {mt5cli-0.5.2 → mt5cli-0.6.0}/docs/api/cli.md +0 -0
  29. {mt5cli-0.5.2 → mt5cli-0.6.0}/docs/api/index.md +0 -0
  30. {mt5cli-0.5.2 → mt5cli-0.6.0}/docs/api/utils.md +0 -0
  31. {mt5cli-0.5.2 → mt5cli-0.6.0}/docs/index.md +0 -0
  32. {mt5cli-0.5.2 → mt5cli-0.6.0}/mkdocs.yml +0 -0
  33. {mt5cli-0.5.2 → mt5cli-0.6.0}/mt5cli/__main__.py +0 -0
  34. {mt5cli-0.5.2 → mt5cli-0.6.0}/mt5cli/cli.py +0 -0
  35. {mt5cli-0.5.2 → mt5cli-0.6.0}/mt5cli/utils.py +0 -0
  36. {mt5cli-0.5.2 → mt5cli-0.6.0}/tests/__init__.py +0 -0
  37. {mt5cli-0.5.2 → mt5cli-0.6.0}/tests/test_cli.py +0 -0
  38. {mt5cli-0.5.2 → mt5cli-0.6.0}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mt5cli
3
- Version: 0.5.2
3
+ Version: 0.6.0
4
4
  Summary: Command-line tool for MetaTrader 5
5
5
  Project-URL: Repository, https://github.com/dceoy/mt5cli.git
6
6
  Author-email: dceoy <dceoy@users.noreply.github.com>
@@ -160,7 +160,24 @@ update_history_with_config(
160
160
  - **Rate view resolution**: use `resolve_rate_view_name()` / `resolve_rate_view_names()` to map symbols and granularities to existing SQLite compatibility views without creating databases. Both accept `None` (or a missing path) and return deterministic default names unless `require_existing=True`.
161
161
  - **Rate view loading**: use `load_rate_data()` / `load_rate_data_from_connection()` to load a SQLite rate table or view into a `DatetimeIndex` DataFrame.
162
162
  - **Multi-series rate loading**: use `build_rate_targets()` to build neutral `RateTarget(symbol, timeframe)` pairs, `resolve_rate_tables()` to map them to table/view names (pass `require_existing=True` for strict resolution), and `load_rate_series_from_sqlite()` to load them into a mapping keyed by `(symbol, integer timeframe)`. The loader requires existing managed views unless `explicit_tables` is supplied, and rejects duplicate `(symbol, timeframe)` targets.
163
- - **Multi-account latest rates**: use `collect_latest_rates_for_accounts()` with `AccountSpec` to read the latest bars for several account groups, merged into a `(symbol, integer timeframe)` mapping.
163
+ - **Multi-account latest rates**: use `collect_latest_rates_for_accounts()` with `AccountSpec` to read the latest bars for several account groups, merged into a `(symbol, integer timeframe)` mapping. For long-running pollers, `collect_latest_rates_for_accounts_with_retries()` adds bounded exponential backoff that retries only `pdmt5.Mt5TradingError` / `pdmt5.Mt5RuntimeError` and re-raises once `retry_count` is exhausted.
164
+ - **Latest closed bars**: use `collect_latest_closed_rates_for_accounts()` when downstream logic must exclude the still-forming current bar. It fetches `count + 1` bars at `start_pos=0`, drops the last row with `drop_forming_rate_bar()`, and validates each series is non-empty. `collect_latest_closed_rates_by_granularity()` returns the same data keyed by `(symbol, granularity_name)` such as `("EURUSD", "M1")`.
165
+
166
+ ```python
167
+ from mt5cli import AccountSpec, collect_latest_closed_rates_by_granularity
168
+
169
+ rates = collect_latest_closed_rates_by_granularity(
170
+ [AccountSpec(symbols=["EURUSD", "GBPUSD"], login=12345)],
171
+ ["M1", "H1"],
172
+ count=500,
173
+ retry_count=3,
174
+ )
175
+ eurusd_m1 = rates["EURUSD", "M1"] # closed bars only
176
+ ```
177
+
178
+ - **Credential resolution**: use `resolve_account_spec()` / `resolve_account_specs()` to merge explicit override values over `AccountSpec` fields and expand `${ENV_VAR}` placeholders (via `substitute_env_placeholders()`), raising `ValueError` for missing variables. This keeps secrets out of plan/config files without coupling to any strategy code.
179
+ - **Throttled history updates**: use `ThrottledHistoryUpdater` to wrap `update_history()` with a minimum `interval_seconds` between successful runs (monotonic clock). Call `should_update()` / `update(client, symbols)` from an application loop; errors propagate by default, or pass `suppress_errors=True` to swallow recoverable `Mt5*Error`/`sqlite3.Error` and let the caller decide logging.
180
+ - **Granularity-keyed rate loading**: `load_rate_series_by_granularity()` builds targets with `build_rate_targets()`, loads them with `load_rate_series_from_sqlite()`, and returns a mapping keyed by `(symbol | None, granularity_name)` such as `("EURUSD", "M1")` to reduce downstream boilerplate.
164
181
  - **MT5 session helper**: use the `mt5_session()` context manager to attach to (or, when `Mt5Config.path` is set, launch) an MT5 terminal, log in, and yield a connected `Mt5CliClient` that shuts down on exit.
165
182
  - **SQLite export helpers**: use `export_dataframe_to_sqlite()` for append mode, optional index export, and post-write deduplication by key columns.
166
183
  - **Recent ticks and margins**: `recent_ticks()` and `minimum_margins()` SDK helpers (and matching CLI commands) cover common downstream read-only queries.
@@ -136,7 +136,24 @@ update_history_with_config(
136
136
  - **Rate view resolution**: use `resolve_rate_view_name()` / `resolve_rate_view_names()` to map symbols and granularities to existing SQLite compatibility views without creating databases. Both accept `None` (or a missing path) and return deterministic default names unless `require_existing=True`.
137
137
  - **Rate view loading**: use `load_rate_data()` / `load_rate_data_from_connection()` to load a SQLite rate table or view into a `DatetimeIndex` DataFrame.
138
138
  - **Multi-series rate loading**: use `build_rate_targets()` to build neutral `RateTarget(symbol, timeframe)` pairs, `resolve_rate_tables()` to map them to table/view names (pass `require_existing=True` for strict resolution), and `load_rate_series_from_sqlite()` to load them into a mapping keyed by `(symbol, integer timeframe)`. The loader requires existing managed views unless `explicit_tables` is supplied, and rejects duplicate `(symbol, timeframe)` targets.
139
- - **Multi-account latest rates**: use `collect_latest_rates_for_accounts()` with `AccountSpec` to read the latest bars for several account groups, merged into a `(symbol, integer timeframe)` mapping.
139
+ - **Multi-account latest rates**: use `collect_latest_rates_for_accounts()` with `AccountSpec` to read the latest bars for several account groups, merged into a `(symbol, integer timeframe)` mapping. For long-running pollers, `collect_latest_rates_for_accounts_with_retries()` adds bounded exponential backoff that retries only `pdmt5.Mt5TradingError` / `pdmt5.Mt5RuntimeError` and re-raises once `retry_count` is exhausted.
140
+ - **Latest closed bars**: use `collect_latest_closed_rates_for_accounts()` when downstream logic must exclude the still-forming current bar. It fetches `count + 1` bars at `start_pos=0`, drops the last row with `drop_forming_rate_bar()`, and validates each series is non-empty. `collect_latest_closed_rates_by_granularity()` returns the same data keyed by `(symbol, granularity_name)` such as `("EURUSD", "M1")`.
141
+
142
+ ```python
143
+ from mt5cli import AccountSpec, collect_latest_closed_rates_by_granularity
144
+
145
+ rates = collect_latest_closed_rates_by_granularity(
146
+ [AccountSpec(symbols=["EURUSD", "GBPUSD"], login=12345)],
147
+ ["M1", "H1"],
148
+ count=500,
149
+ retry_count=3,
150
+ )
151
+ eurusd_m1 = rates["EURUSD", "M1"] # closed bars only
152
+ ```
153
+
154
+ - **Credential resolution**: use `resolve_account_spec()` / `resolve_account_specs()` to merge explicit override values over `AccountSpec` fields and expand `${ENV_VAR}` placeholders (via `substitute_env_placeholders()`), raising `ValueError` for missing variables. This keeps secrets out of plan/config files without coupling to any strategy code.
155
+ - **Throttled history updates**: use `ThrottledHistoryUpdater` to wrap `update_history()` with a minimum `interval_seconds` between successful runs (monotonic clock). Call `should_update()` / `update(client, symbols)` from an application loop; errors propagate by default, or pass `suppress_errors=True` to swallow recoverable `Mt5*Error`/`sqlite3.Error` and let the caller decide logging.
156
+ - **Granularity-keyed rate loading**: `load_rate_series_by_granularity()` builds targets with `build_rate_targets()`, loads them with `load_rate_series_from_sqlite()`, and returns a mapping keyed by `(symbol | None, granularity_name)` such as `("EURUSD", "M1")` to reduce downstream boilerplate.
140
157
  - **MT5 session helper**: use the `mt5_session()` context manager to attach to (or, when `Mt5Config.path` is set, launch) an MT5 terminal, log in, and yield a connected `Mt5CliClient` that shuts down on exit.
141
158
  - **SQLite export helpers**: use `export_dataframe_to_sqlite()` for append mode, optional index export, and post-write deduplication by key columns.
142
159
  - **Recent ticks and margins**: `recent_ticks()` and `minimum_margins()` SDK helpers (and matching CLI commands) cover common downstream read-only queries.
@@ -215,3 +215,15 @@ frame = series["EURUSD", 1] # keyed by (symbol, integer timeframe)
215
215
  requires existing managed `rate_*` compatibility views and raises
216
216
  `ValueError` when they are missing. Duplicate `(symbol, timeframe)` targets
217
217
  are rejected.
218
+ - `load_rate_series_by_granularity()` is a thin wrapper that builds the targets,
219
+ loads the series, and rekeys the result by granularity name to avoid
220
+ converting integer timeframes downstream:
221
+
222
+ ```python
223
+ from mt5cli import load_rate_series_by_granularity
224
+
225
+ series = load_rate_series_by_granularity(
226
+ "history.db", ["EURUSD"], ["M1", "H1"], count=1000
227
+ )
228
+ frame = series["EURUSD", "M1"] # keyed by (symbol | None, granularity_name)
229
+ ```
@@ -0,0 +1,103 @@
1
+ # SDK Module
2
+
3
+ ::: mt5cli.sdk
4
+
5
+ ## Resilient multi-account orchestration
6
+
7
+ The SDK ships strategy-agnostic helpers for building long-running collectors on
8
+ top of the read-only client. None of them depend on a particular trading
9
+ application.
10
+
11
+ ### Retrying transient rate collection
12
+
13
+ `collect_latest_rates_for_accounts_with_retries()` wraps
14
+ `collect_latest_rates_for_accounts()` with bounded exponential backoff. Only
15
+ `pdmt5.Mt5TradingError` and `pdmt5.Mt5RuntimeError` are retried; the final
16
+ failure is re-raised once `retry_count` is exhausted.
17
+
18
+ ```python
19
+ from mt5cli import AccountSpec, collect_latest_rates_for_accounts_with_retries
20
+
21
+ accounts = [AccountSpec(symbols=["EURUSD"], login=12345)]
22
+ rates = collect_latest_rates_for_accounts_with_retries(
23
+ accounts,
24
+ ["M1", "H1"],
25
+ count=500,
26
+ retry_count=3,
27
+ backoff_base=2, # sleeps 2s, 4s, 8s between attempts
28
+ )
29
+ ```
30
+
31
+ ### Latest closed rate bars
32
+
33
+ MetaTrader 5 `start_pos=0` includes the still-forming current bar as the last
34
+ row. `collect_latest_closed_rates_for_accounts()` fetches `count + 1` bars,
35
+ drops that row with `drop_forming_rate_bar()`, and validates each series is
36
+ non-empty. Use `collect_latest_closed_rates_by_granularity()` when callers
37
+ prefer keys such as `("EURUSD", "M1")` instead of integer timeframes.
38
+
39
+ ```python
40
+ from mt5cli import AccountSpec, collect_latest_closed_rates_by_granularity
41
+
42
+ rates = collect_latest_closed_rates_by_granularity(
43
+ [AccountSpec(symbols=["EURUSD"], login=12345)],
44
+ ["M1", "H1"],
45
+ count=500,
46
+ retry_count=3,
47
+ )
48
+ closed_m1 = rates["EURUSD", "M1"]
49
+ ```
50
+
51
+ ### Resolving credentials and `${ENV_VAR}` placeholders
52
+
53
+ `resolve_account_spec()` / `resolve_account_specs()` merge explicit override
54
+ values over `AccountSpec` fields and expand `${ENV_VAR}` placeholders, keeping
55
+ secrets out of plan/config files. A missing environment variable raises
56
+ `ValueError`.
57
+
58
+ ```python
59
+ import os
60
+
61
+ from mt5cli import AccountSpec, resolve_account_specs
62
+
63
+ os.environ["MT5_LOGIN"] = "12345"
64
+ os.environ["MT5_PASSWORD"] = "secret"
65
+ accounts = [
66
+ AccountSpec(symbols=["EURUSD"], login="${MT5_LOGIN}", password="${MT5_PASSWORD}")
67
+ ]
68
+
69
+ resolved = resolve_account_specs(accounts, server="Broker-Demo")
70
+ # resolved[0].login == "12345", resolved[0].server == "Broker-Demo"
71
+ ```
72
+
73
+ ### Throttled incremental history updates
74
+
75
+ `ThrottledHistoryUpdater` wraps `update_history()` with a minimum interval
76
+ between successful runs (using a monotonic clock), so an application loop can
77
+ call it every iteration without over-fetching.
78
+
79
+ ```python
80
+ from pdmt5 import Mt5Config, Mt5DataClient
81
+
82
+ from mt5cli import Dataset, ThrottledHistoryUpdater
83
+
84
+ updater = ThrottledHistoryUpdater(
85
+ output="history.db",
86
+ datasets={Dataset.rates},
87
+ timeframes=["M1"],
88
+ interval_seconds=60, # <= 0 updates on every call
89
+ )
90
+
91
+ client = Mt5DataClient(config=Mt5Config(login=12345))
92
+ client.initialize_and_login_mt5()
93
+ try:
94
+ while True:
95
+ updater.update(client, ["EURUSD", "GBPUSD"]) # no-op until 60s elapse
96
+ # ... do other work; break when shutting down ...
97
+ finally:
98
+ client.shutdown()
99
+ ```
100
+
101
+ By default `Mt5TradingError`, `Mt5RuntimeError`, and `sqlite3.Error` propagate so
102
+ the caller controls logging; pass `suppress_errors=True` to swallow them and
103
+ return `False` without advancing the throttle.
@@ -6,8 +6,10 @@ from .history import (
6
6
  RateTarget,
7
7
  build_rate_targets,
8
8
  build_rate_view_name,
9
+ drop_forming_rate_bar,
9
10
  load_rate_data,
10
11
  load_rate_data_from_connection,
12
+ load_rate_series_by_granularity,
11
13
  load_rate_series_from_sqlite,
12
14
  resolve_history_datasets,
13
15
  resolve_history_tick_flags,
@@ -19,11 +21,15 @@ from .history import (
19
21
  from .sdk import (
20
22
  AccountSpec,
21
23
  Mt5CliClient,
24
+ ThrottledHistoryUpdater,
22
25
  account_info,
23
26
  build_config,
24
27
  collect_history,
28
+ collect_latest_closed_rates_by_granularity,
29
+ collect_latest_closed_rates_for_accounts,
25
30
  collect_latest_rates,
26
31
  collect_latest_rates_for_accounts,
32
+ collect_latest_rates_for_accounts_with_retries,
27
33
  copy_rates_from,
28
34
  copy_rates_from_pos,
29
35
  copy_rates_range,
@@ -42,6 +48,9 @@ from .sdk import (
42
48
  positions,
43
49
  recent_history_deals,
44
50
  recent_ticks,
51
+ resolve_account_spec,
52
+ resolve_account_specs,
53
+ substitute_env_placeholders,
45
54
  symbol_info,
46
55
  symbol_info_tick,
47
56
  symbols,
@@ -75,19 +84,24 @@ __all__ = [
75
84
  "IfExists",
76
85
  "Mt5CliClient",
77
86
  "RateTarget",
87
+ "ThrottledHistoryUpdater",
78
88
  "account_info",
79
89
  "build_config",
80
90
  "build_rate_targets",
81
91
  "build_rate_view_name",
82
92
  "collect_history",
93
+ "collect_latest_closed_rates_by_granularity",
94
+ "collect_latest_closed_rates_for_accounts",
83
95
  "collect_latest_rates",
84
96
  "collect_latest_rates_for_accounts",
97
+ "collect_latest_rates_for_accounts_with_retries",
85
98
  "copy_rates_from",
86
99
  "copy_rates_from_pos",
87
100
  "copy_rates_range",
88
101
  "copy_ticks_from",
89
102
  "copy_ticks_range",
90
103
  "detect_format",
104
+ "drop_forming_rate_bar",
91
105
  "export_dataframe",
92
106
  "export_dataframe_to_sqlite",
93
107
  "history_deals",
@@ -96,6 +110,7 @@ __all__ = [
96
110
  "latest_rates",
97
111
  "load_rate_data",
98
112
  "load_rate_data_from_connection",
113
+ "load_rate_series_by_granularity",
99
114
  "load_rate_series_from_sqlite",
100
115
  "market_book",
101
116
  "minimum_margins",
@@ -110,12 +125,15 @@ __all__ = [
110
125
  "positions",
111
126
  "recent_history_deals",
112
127
  "recent_ticks",
128
+ "resolve_account_spec",
129
+ "resolve_account_specs",
113
130
  "resolve_history_datasets",
114
131
  "resolve_history_tick_flags",
115
132
  "resolve_history_timeframes",
116
133
  "resolve_rate_tables",
117
134
  "resolve_rate_view_name",
118
135
  "resolve_rate_view_names",
136
+ "substitute_env_placeholders",
119
137
  "symbol_info",
120
138
  "symbol_info_tick",
121
139
  "symbols",
@@ -106,6 +106,23 @@ def resolve_granularity_name(timeframe: int) -> str:
106
106
  return str(timeframe)
107
107
 
108
108
 
109
+ def drop_forming_rate_bar(df_rate: pd.DataFrame) -> pd.DataFrame:
110
+ """Return closed bars from chronologically ordered MT5 rate data.
111
+
112
+ MetaTrader 5 ``copy_rates_from_pos(start_pos=0)`` includes the still-forming
113
+ current bar as the last row. Slice it off so downstream logic only sees
114
+ completed bars. Empty frames and single-row frames return empty results.
115
+
116
+ Args:
117
+ df_rate: Rate data ordered oldest-to-newest with the forming bar last.
118
+
119
+ Returns:
120
+ A new DataFrame with all rows except the last. Index and columns are
121
+ preserved. The input frame is not modified.
122
+ """
123
+ return df_rate.iloc[:-1].copy()
124
+
125
+
109
126
  def build_rate_view_name(
110
127
  *,
111
128
  symbol: str,
@@ -706,6 +723,55 @@ def load_rate_series_from_sqlite(
706
723
  conn.close()
707
724
 
708
725
 
726
+ def load_rate_series_by_granularity(
727
+ conn_or_path: SqliteConnOrPath,
728
+ symbols: Sequence[str],
729
+ granularities: Sequence[int | str],
730
+ count: int,
731
+ *,
732
+ explicit_tables: Sequence[str] | None = None,
733
+ allow_missing_symbol: bool = False,
734
+ ) -> dict[tuple[str | None, str], pd.DataFrame]:
735
+ """Load rate series keyed by symbol and string granularity name.
736
+
737
+ Builds targets with :func:`build_rate_targets` and loads them with
738
+ :func:`load_rate_series_from_sqlite`, then rekeys the result by granularity
739
+ name (for example ``M1``) instead of the integer timeframe to reduce
740
+ downstream boilerplate.
741
+
742
+ Args:
743
+ conn_or_path: SQLite database path or open connection.
744
+ symbols: MT5 symbol names. May be empty when ``allow_missing_symbol``.
745
+ granularities: MT5 timeframes as integers or names (for example ``M1``).
746
+ count: Number of most recent rows to load per series.
747
+ explicit_tables: Optional explicit table or view names matching the
748
+ built targets in row-major order. Required when symbols are omitted.
749
+ allow_missing_symbol: When True and ``symbols`` is empty, build targets
750
+ with ``symbol=None`` for each granularity instead of raising.
751
+
752
+ Returns:
753
+ Mapping keyed by ``(symbol | None, granularity_name)`` to each rate
754
+ DataFrame. Propagates ``ValueError`` (via :func:`build_rate_targets` and
755
+ :func:`load_rate_series_from_sqlite`) when inputs are empty or invalid,
756
+ table resolution fails, or duplicate targets are present.
757
+ """
758
+ targets = build_rate_targets(
759
+ symbols,
760
+ granularities,
761
+ allow_missing_symbol=allow_missing_symbol,
762
+ )
763
+ series = load_rate_series_from_sqlite(
764
+ conn_or_path,
765
+ targets,
766
+ count,
767
+ explicit_tables=explicit_tables,
768
+ )
769
+ return {
770
+ (symbol, resolve_granularity_name(timeframe)): frame
771
+ for (symbol, timeframe), frame in series.items()
772
+ }
773
+
774
+
709
775
  def get_table_columns(conn: sqlite3.Connection, table: str) -> set[str]:
710
776
  """Return existing SQLite columns for a table."""
711
777
  quoted_table = quote_sqlite_identifier(table)