opencode-pyneruntime 6.6.4__py3-none-any.whl
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.
- opencode_pyneruntime-6.6.4.dist-info/METADATA +281 -0
- opencode_pyneruntime-6.6.4.dist-info/RECORD +261 -0
- opencode_pyneruntime-6.6.4.dist-info/WHEEL +5 -0
- opencode_pyneruntime-6.6.4.dist-info/entry_points.txt +6 -0
- opencode_pyneruntime-6.6.4.dist-info/licenses/LICENSE +201 -0
- opencode_pyneruntime-6.6.4.dist-info/licenses/NOTICE +21 -0
- opencode_pyneruntime-6.6.4.dist-info/top_level.txt +1 -0
- pynecore/__init__.py +6 -0
- pynecore/cli/__init__.py +2 -0
- pynecore/cli/app.py +238 -0
- pynecore/cli/commands/__init__.py +343 -0
- pynecore/cli/commands/benchmark.py +186 -0
- pynecore/cli/commands/compile.py +198 -0
- pynecore/cli/commands/data.py +857 -0
- pynecore/cli/commands/debug.py +63 -0
- pynecore/cli/commands/optimize.py +956 -0
- pynecore/cli/commands/plugin.py +242 -0
- pynecore/cli/commands/run.py +2006 -0
- pynecore/cli/pluggable.py +132 -0
- pynecore/cli/utils/__init__.py +0 -0
- pynecore/cli/utils/api_error_handler.py +168 -0
- pynecore/cli/utils/broker_picker.py +330 -0
- pynecore/cli/utils/error_hook.py +28 -0
- pynecore/cli/utils/keyreader.py +178 -0
- pynecore/cli/utils/provider_picker.py +19 -0
- pynecore/cli/utils/symbol_browser.py +1149 -0
- pynecore/core/__init__.py +0 -0
- pynecore/core/aggregator.py +257 -0
- pynecore/core/bar_magnifier.py +168 -0
- pynecore/core/broker/__init__.py +64 -0
- pynecore/core/broker/defaults.py +113 -0
- pynecore/core/broker/disappearance.py +927 -0
- pynecore/core/broker/emulator.py +345 -0
- pynecore/core/broker/exceptions.py +346 -0
- pynecore/core/broker/idempotency.py +401 -0
- pynecore/core/broker/intent_builder.py +334 -0
- pynecore/core/broker/journal.py +1785 -0
- pynecore/core/broker/models.py +1600 -0
- pynecore/core/broker/native_failsafe_manager.py +1436 -0
- pynecore/core/broker/one_way_emulator.py +1128 -0
- pynecore/core/broker/position.py +787 -0
- pynecore/core/broker/run_identity.py +126 -0
- pynecore/core/broker/software_entry_stop_engine.py +351 -0
- pynecore/core/broker/software_partial_bracket_engine.py +1379 -0
- pynecore/core/broker/spot_inventory.py +1327 -0
- pynecore/core/broker/storage.py +2655 -0
- pynecore/core/broker/store_helpers.py +2161 -0
- pynecore/core/broker/sync_engine.py +16070 -0
- pynecore/core/broker/validation.py +382 -0
- pynecore/core/class_property.py +7 -0
- pynecore/core/config.py +392 -0
- pynecore/core/csv_file.py +547 -0
- pynecore/core/currency.py +262 -0
- pynecore/core/data_converter.py +1002 -0
- pynecore/core/datetime.py +296 -0
- pynecore/core/download_info.py +71 -0
- pynecore/core/download_runner.py +274 -0
- pynecore/core/htf_aggregator.py +181 -0
- pynecore/core/import_hook.py +358 -0
- pynecore/core/instance_state.py +494 -0
- pynecore/core/live_ltf_collector.py +442 -0
- pynecore/core/live_ltf_window.py +189 -0
- pynecore/core/live_runner.py +1347 -0
- pynecore/core/module_property.py +26 -0
- pynecore/core/ohlcv_file.py +1888 -0
- pynecore/core/overload.py +371 -0
- pynecore/core/pine_cast.py +113 -0
- pynecore/core/pine_export.py +95 -0
- pynecore/core/pine_method.py +244 -0
- pynecore/core/pine_range.py +86 -0
- pynecore/core/pine_udt.py +69 -0
- pynecore/core/plugin/__init__.py +394 -0
- pynecore/core/plugin/broker.py +781 -0
- pynecore/core/plugin/cli.py +96 -0
- pynecore/core/plugin/live_provider.py +208 -0
- pynecore/core/plugin/provider.py +331 -0
- pynecore/core/provider_string.py +148 -0
- pynecore/core/random.py +40 -0
- pynecore/core/resampler.py +686 -0
- pynecore/core/safe_convert.py +64 -0
- pynecore/core/script.py +1011 -0
- pynecore/core/script_runner.py +3202 -0
- pynecore/core/security.py +1749 -0
- pynecore/core/security_process.py +1253 -0
- pynecore/core/security_shm.py +456 -0
- pynecore/core/series.py +417 -0
- pynecore/core/strategy_stats.py +669 -0
- pynecore/core/symbol_map.py +134 -0
- pynecore/core/syminfo.py +505 -0
- pynecore/core/viz.py +591 -0
- pynecore/lib/__init__.py +1771 -0
- pynecore/lib/_fixnan.py +32 -0
- pynecore/lib/_math_stateful.py +202 -0
- pynecore/lib/_timeframe_change.py +101 -0
- pynecore/lib/adjustment.py +6 -0
- pynecore/lib/alert.py +39 -0
- pynecore/lib/alert.pyi +14 -0
- pynecore/lib/array.py +1051 -0
- pynecore/lib/barmerge.py +60 -0
- pynecore/lib/barstate.py +30 -0
- pynecore/lib/box.py +415 -0
- pynecore/lib/chart.py +128 -0
- pynecore/lib/color.py +152 -0
- pynecore/lib/color.pyi +50 -0
- pynecore/lib/currency.py +62 -0
- pynecore/lib/dayofweek.py +36 -0
- pynecore/lib/dayofweek.pyi +18 -0
- pynecore/lib/display.py +8 -0
- pynecore/lib/dividends.py +9 -0
- pynecore/lib/earnings.py +11 -0
- pynecore/lib/extend.py +6 -0
- pynecore/lib/font.py +5 -0
- pynecore/lib/footprint.py +79 -0
- pynecore/lib/format.py +11 -0
- pynecore/lib/hline.py +67 -0
- pynecore/lib/hline.pyi +24 -0
- pynecore/lib/label.py +409 -0
- pynecore/lib/line.py +433 -0
- pynecore/lib/linefill.py +93 -0
- pynecore/lib/location.py +11 -0
- pynecore/lib/log.py +362 -0
- pynecore/lib/map.py +150 -0
- pynecore/lib/math.py +385 -0
- pynecore/lib/matrix.py +708 -0
- pynecore/lib/order.py +8 -0
- pynecore/lib/pivotpointtype.py +8 -0
- pynecore/lib/plot.py +95 -0
- pynecore/lib/plot.pyi +33 -0
- pynecore/lib/polyline.py +91 -0
- pynecore/lib/position.py +15 -0
- pynecore/lib/request.py +281 -0
- pynecore/lib/runtime.py +5 -0
- pynecore/lib/scale.py +9 -0
- pynecore/lib/session.py +267 -0
- pynecore/lib/session.pyi +12 -0
- pynecore/lib/shape.py +18 -0
- pynecore/lib/size.py +12 -0
- pynecore/lib/splits.py +4 -0
- pynecore/lib/strategy/__init__.py +4778 -0
- pynecore/lib/strategy/closedtrades.py +347 -0
- pynecore/lib/strategy/closedtrades.pyi +53 -0
- pynecore/lib/strategy/commission.py +9 -0
- pynecore/lib/strategy/direction.py +9 -0
- pynecore/lib/strategy/oca.py +13 -0
- pynecore/lib/strategy/opentrades.py +281 -0
- pynecore/lib/strategy/opentrades.pyi +49 -0
- pynecore/lib/strategy/risk.py +109 -0
- pynecore/lib/string.py +649 -0
- pynecore/lib/syminfo.py +84 -0
- pynecore/lib/ta.py +2230 -0
- pynecore/lib/table.py +290 -0
- pynecore/lib/text.py +17 -0
- pynecore/lib/ticker.py +207 -0
- pynecore/lib/timeframe.py +293 -0
- pynecore/lib/volume_row.py +67 -0
- pynecore/lib/xloc.py +4 -0
- pynecore/lib/yloc.py +5 -0
- pynecore/providers/__init__.py +0 -0
- pynecore/providers/ccxt.py +664 -0
- pynecore/providers/replay.py +187 -0
- pynecore/pynesys/__init__.py +0 -0
- pynecore/pynesys/api.py +498 -0
- pynecore/pynesys/compiler.py +112 -0
- pynecore/standalone.py +99 -0
- pynecore/testing/__init__.py +1 -0
- pynecore/testing/broker_lab/__init__.py +41 -0
- pynecore/testing/broker_lab/__main__.py +5 -0
- pynecore/testing/broker_lab/cli.py +87 -0
- pynecore/testing/broker_lab/generate.py +47 -0
- pynecore/testing/broker_lab/model.py +84 -0
- pynecore/testing/broker_lab/reference.py +645 -0
- pynecore/testing/broker_lab/runner.py +372 -0
- pynecore/testing/broker_lab/scheduler.py +50 -0
- pynecore/testing/broker_lab/subprocess.py +73 -0
- pynecore/transformers/__init__.py +0 -0
- pynecore/transformers/builtin_shadow.py +136 -0
- pynecore/transformers/closure_arguments_transformer.py +428 -0
- pynecore/transformers/display_rewrite.py +140 -0
- pynecore/transformers/dynamic_default.py +147 -0
- pynecore/transformers/function_isolation.py +757 -0
- pynecore/transformers/import_lifter.py +61 -0
- pynecore/transformers/import_normalizer.py +328 -0
- pynecore/transformers/inline_series_hoist.py +178 -0
- pynecore/transformers/input_transformer.py +175 -0
- pynecore/transformers/lib_series.py +201 -0
- pynecore/transformers/locations.py +70 -0
- pynecore/transformers/module_properties.json +3387 -0
- pynecore/transformers/module_property.py +221 -0
- pynecore/transformers/ne_guard.py +70 -0
- pynecore/transformers/persistent.py +320 -0
- pynecore/transformers/persistent_series.py +76 -0
- pynecore/transformers/safe_convert_transformer.py +97 -0
- pynecore/transformers/safe_division_transformer.py +95 -0
- pynecore/transformers/script_requirements.py +308 -0
- pynecore/transformers/security.py +752 -0
- pynecore/transformers/security_instantiation.py +274 -0
- pynecore/transformers/series.py +275 -0
- pynecore/transformers/slot_layout.py +381 -0
- pynecore/transformers/type_checking_stripper.py +25 -0
- pynecore/transformers/unused_series_detector.py +267 -0
- pynecore/types/__init__.py +21 -0
- pynecore/types/alert.py +5 -0
- pynecore/types/barmerge.py +5 -0
- pynecore/types/base.py +39 -0
- pynecore/types/box.py +37 -0
- pynecore/types/chart.py +17 -0
- pynecore/types/color.py +107 -0
- pynecore/types/currency.py +5 -0
- pynecore/types/datetime.py +6 -0
- pynecore/types/display.py +5 -0
- pynecore/types/dividends.py +5 -0
- pynecore/types/earnings.py +5 -0
- pynecore/types/extend.py +5 -0
- pynecore/types/font.py +5 -0
- pynecore/types/footprint.py +41 -0
- pynecore/types/format.py +5 -0
- pynecore/types/hline.py +24 -0
- pynecore/types/ib_persistent.py +8 -0
- pynecore/types/ib_persistent.pyi +10 -0
- pynecore/types/label.py +35 -0
- pynecore/types/line.py +32 -0
- pynecore/types/linefill.py +13 -0
- pynecore/types/location.py +5 -0
- pynecore/types/matrix.py +999 -0
- pynecore/types/na.py +237 -0
- pynecore/types/na.pyi +83 -0
- pynecore/types/ohlcv.py +12 -0
- pynecore/types/order.py +5 -0
- pynecore/types/persistent.py +8 -0
- pynecore/types/persistent.pyi +13 -0
- pynecore/types/pine_types.py +11 -0
- pynecore/types/pine_types.pyi +15 -0
- pynecore/types/pivotpointtype.py +5 -0
- pynecore/types/plot.py +12 -0
- pynecore/types/plot_meta.py +60 -0
- pynecore/types/polyline.py +40 -0
- pynecore/types/position.py +5 -0
- pynecore/types/scale.py +5 -0
- pynecore/types/script_type.py +15 -0
- pynecore/types/series.py +23 -0
- pynecore/types/series.pyi +19 -0
- pynecore/types/session.py +35 -0
- pynecore/types/shape.py +5 -0
- pynecore/types/size.py +5 -0
- pynecore/types/source.py +33 -0
- pynecore/types/splits.py +5 -0
- pynecore/types/strategy.py +45 -0
- pynecore/types/table.py +87 -0
- pynecore/types/text.py +13 -0
- pynecore/types/type_checker.py +7 -0
- pynecore/types/type_checker.pyi +48 -0
- pynecore/types/volume_row.py +36 -0
- pynecore/types/weekdays.py +11 -0
- pynecore/types/xloc.py +5 -0
- pynecore/types/yloc.py +5 -0
- pynecore/utils/__init__.py +0 -0
- pynecore/utils/file_utils.py +50 -0
- pynecore/utils/rich/__init__.py +0 -0
- pynecore/utils/rich/date_column.py +25 -0
- pynecore/utils/sequence_view.py +92 -0
- pynecore/utils/stdlib_checker.py +17 -0
pynecore/core/config.py
ADDED
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
"""
|
|
2
|
+
General-purpose dataclass-based configuration with self-healing TOML files.
|
|
3
|
+
|
|
4
|
+
Generates a TOML configuration file from a Python dataclass definition.
|
|
5
|
+
On each run the file is regenerated from the dataclass: user-modified values
|
|
6
|
+
are preserved while the structure always reflects the current field set.
|
|
7
|
+
|
|
8
|
+
Convention::
|
|
9
|
+
|
|
10
|
+
#key = value — default / unmodified (commented out)
|
|
11
|
+
key = value — user-modified (uncommented)
|
|
12
|
+
#key = — None value
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import ast
|
|
16
|
+
import dataclasses
|
|
17
|
+
import inspect
|
|
18
|
+
import textwrap
|
|
19
|
+
import tomllib
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Any, cast
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class mlstr(str):
|
|
25
|
+
"""TOML multi-line string marker.
|
|
26
|
+
|
|
27
|
+
Annotate a config field with this type to force the writer to render
|
|
28
|
+
the value as a multi-line literal block (``'''…'''``), even when the
|
|
29
|
+
default is empty or a single line. Useful for fields where users will
|
|
30
|
+
paste multi-line content (PEM keys, certificates, SSH keys, etc.) —
|
|
31
|
+
the commented default already shows the paste markers, so users do
|
|
32
|
+
not have to know TOML's multi-line literal syntax to fill it in.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def format_value(value: str | int | float | bool) -> str:
|
|
37
|
+
"""
|
|
38
|
+
Format a Python value as a TOML value string.
|
|
39
|
+
|
|
40
|
+
Handles the four TOML-native types: ``str``, ``int``, ``float``, ``bool``.
|
|
41
|
+
``mlstr`` values, and any plain string containing ``\\n``, are emitted
|
|
42
|
+
as TOML multi-line literal blocks (``'''…'''``) so PEM keys,
|
|
43
|
+
certificates and similar payloads stay readable in the generated
|
|
44
|
+
config. Strings that contain ``'''`` fall back to a single-line
|
|
45
|
+
escaped form.
|
|
46
|
+
This function is intentionally public so that other modules (e.g.
|
|
47
|
+
``core.script``) can reuse it for consistent TOML formatting.
|
|
48
|
+
|
|
49
|
+
:param value: The value to format.
|
|
50
|
+
:return: TOML-formatted string representation.
|
|
51
|
+
"""
|
|
52
|
+
if isinstance(value, bool):
|
|
53
|
+
return str(value).lower()
|
|
54
|
+
if isinstance(value, int):
|
|
55
|
+
return str(value)
|
|
56
|
+
if isinstance(value, float):
|
|
57
|
+
return str(value)
|
|
58
|
+
if isinstance(value, mlstr) and "'''" not in value:
|
|
59
|
+
return f"'''\n{value}'''"
|
|
60
|
+
if isinstance(value, str):
|
|
61
|
+
if '\n' in value and "'''" not in value:
|
|
62
|
+
return f"'''\n{value}'''"
|
|
63
|
+
escaped = (
|
|
64
|
+
value
|
|
65
|
+
.replace('\\', '\\\\')
|
|
66
|
+
.replace('"', '\\"')
|
|
67
|
+
.replace('\n', '\\n')
|
|
68
|
+
.replace('\r', '\\r')
|
|
69
|
+
)
|
|
70
|
+
return f'"{escaped}"'
|
|
71
|
+
return str(value)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _format_for_toml(value: Any) -> str:
|
|
75
|
+
"""Format any supported value as a TOML literal.
|
|
76
|
+
|
|
77
|
+
Wraps :func:`format_value` for the four scalar types and adds TOML
|
|
78
|
+
inline-table / array support so ``dict`` and ``list`` fields (e.g.
|
|
79
|
+
``LiveProviderConfig.symbol_map``) round-trip through the
|
|
80
|
+
self-healing config writer.
|
|
81
|
+
"""
|
|
82
|
+
if isinstance(value, dict):
|
|
83
|
+
if not value:
|
|
84
|
+
return "{}"
|
|
85
|
+
parts = [
|
|
86
|
+
f"{format_value(str(k))} = {_format_for_toml(v)}"
|
|
87
|
+
for k, v in value.items()
|
|
88
|
+
]
|
|
89
|
+
return "{ " + ", ".join(parts) + " }"
|
|
90
|
+
if isinstance(value, list):
|
|
91
|
+
if not value:
|
|
92
|
+
return "[]"
|
|
93
|
+
return "[" + ", ".join(_format_for_toml(v) for v in value) + "]"
|
|
94
|
+
return format_value(value)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _is_mlstr_field(f: dataclasses.Field) -> bool:
|
|
98
|
+
"""Check if a dataclass field is annotated as ``mlstr``.
|
|
99
|
+
|
|
100
|
+
Relies on ``f.type`` being a real type object (not a forward-ref
|
|
101
|
+
string) — guaranteed in this project because ``from __future__ import
|
|
102
|
+
annotations`` is intentionally not used.
|
|
103
|
+
"""
|
|
104
|
+
t = f.type
|
|
105
|
+
return isinstance(t, type) and issubclass(t, mlstr)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _emit_assignment(
|
|
109
|
+
lines: list[str],
|
|
110
|
+
name: str,
|
|
111
|
+
formatted: str,
|
|
112
|
+
*,
|
|
113
|
+
commented: bool,
|
|
114
|
+
) -> None:
|
|
115
|
+
"""Append a ``name = value`` assignment to ``lines``.
|
|
116
|
+
|
|
117
|
+
For multi-line formatted values (e.g. ``'''…'''``), every continuation
|
|
118
|
+
line is prefixed with ``#`` when ``commented`` is true so the whole
|
|
119
|
+
block is one TOML comment, not a half-commented stray.
|
|
120
|
+
"""
|
|
121
|
+
parts = formatted.split('\n')
|
|
122
|
+
prefix = '#' if commented else ''
|
|
123
|
+
lines.append(f"{prefix}{name} = {parts[0]}")
|
|
124
|
+
for cont in parts[1:]:
|
|
125
|
+
lines.append(f"{prefix}{cont}")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def extract_field_docs(config_cls: type) -> dict[str, str]:
|
|
129
|
+
"""
|
|
130
|
+
Extract attribute docstrings from a dataclass source via AST parsing.
|
|
131
|
+
|
|
132
|
+
Looks for ``Expr(Constant(str))`` nodes immediately following
|
|
133
|
+
``AnnAssign`` nodes in the class body (PEP 257 attribute docstrings).
|
|
134
|
+
Walks the MRO in reverse order so docstrings on inherited fields are
|
|
135
|
+
picked up too — a subclass-level docstring overrides the inherited
|
|
136
|
+
one for the same field name.
|
|
137
|
+
|
|
138
|
+
:param config_cls: The dataclass type to inspect.
|
|
139
|
+
:return: Mapping of field name to its docstring.
|
|
140
|
+
"""
|
|
141
|
+
docs: dict[str, str] = {}
|
|
142
|
+
for cls in reversed(config_cls.__mro__):
|
|
143
|
+
if cls is object:
|
|
144
|
+
continue
|
|
145
|
+
try:
|
|
146
|
+
source = textwrap.dedent(inspect.getsource(cls))
|
|
147
|
+
except (OSError, TypeError):
|
|
148
|
+
continue
|
|
149
|
+
|
|
150
|
+
try:
|
|
151
|
+
tree = ast.parse(source)
|
|
152
|
+
except SyntaxError:
|
|
153
|
+
continue
|
|
154
|
+
|
|
155
|
+
class_def = None
|
|
156
|
+
for node in ast.walk(tree):
|
|
157
|
+
if isinstance(node, ast.ClassDef) and node.name == cls.__name__:
|
|
158
|
+
class_def = node
|
|
159
|
+
break
|
|
160
|
+
|
|
161
|
+
if class_def is None:
|
|
162
|
+
continue
|
|
163
|
+
|
|
164
|
+
body = class_def.body
|
|
165
|
+
for i, node in enumerate(body):
|
|
166
|
+
if isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name):
|
|
167
|
+
field_name = node.target.id
|
|
168
|
+
if i + 1 < len(body):
|
|
169
|
+
next_node = body[i + 1]
|
|
170
|
+
if (
|
|
171
|
+
isinstance(next_node, ast.Expr)
|
|
172
|
+
and isinstance(next_node.value, ast.Constant)
|
|
173
|
+
and isinstance(next_node.value.value, str)
|
|
174
|
+
):
|
|
175
|
+
docs[field_name] = next_node.value.value
|
|
176
|
+
|
|
177
|
+
return docs
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def generate_toml(
|
|
181
|
+
config_cls: type,
|
|
182
|
+
user_values: dict | None = None,
|
|
183
|
+
) -> str:
|
|
184
|
+
"""
|
|
185
|
+
Generate a TOML string from a dataclass definition.
|
|
186
|
+
|
|
187
|
+
Fields with user-modified values are written uncommented. Fields at their
|
|
188
|
+
default value are written as comments (``#key = value``).
|
|
189
|
+
|
|
190
|
+
:param config_cls: The config dataclass type.
|
|
191
|
+
:param user_values: User-modified values to write uncommented.
|
|
192
|
+
:return: Generated TOML content string.
|
|
193
|
+
"""
|
|
194
|
+
field_docs = extract_field_docs(config_cls)
|
|
195
|
+
lines: list[str] = []
|
|
196
|
+
|
|
197
|
+
class_doc = config_cls.__doc__
|
|
198
|
+
if class_doc:
|
|
199
|
+
for doc_line in class_doc.strip().splitlines():
|
|
200
|
+
stripped = doc_line.strip()
|
|
201
|
+
lines.append(f"# {stripped}" if stripped else "#")
|
|
202
|
+
|
|
203
|
+
for f in dataclasses.fields(cast(Any, config_cls)):
|
|
204
|
+
name = f.name
|
|
205
|
+
default = f.default
|
|
206
|
+
is_ml = _is_mlstr_field(f)
|
|
207
|
+
|
|
208
|
+
# Resolve ``field(default_factory=...)`` to an actual default value
|
|
209
|
+
# so the commented placeholder shows real TOML syntax (e.g.
|
|
210
|
+
# ``#symbol_map = {}``) instead of a bare ``#symbol_map =``.
|
|
211
|
+
if default is dataclasses.MISSING and f.default_factory is not dataclasses.MISSING:
|
|
212
|
+
try:
|
|
213
|
+
default = f.default_factory()
|
|
214
|
+
except Exception: # noqa: BLE001
|
|
215
|
+
default = dataclasses.MISSING
|
|
216
|
+
|
|
217
|
+
lines.append("")
|
|
218
|
+
|
|
219
|
+
if name in field_docs:
|
|
220
|
+
for doc_line in field_docs[name].strip().splitlines():
|
|
221
|
+
lines.append(f"# {doc_line.strip()}")
|
|
222
|
+
|
|
223
|
+
if user_values and name in user_values:
|
|
224
|
+
value = user_values[name]
|
|
225
|
+
if is_ml and not isinstance(value, mlstr):
|
|
226
|
+
value = mlstr(value)
|
|
227
|
+
_emit_assignment(lines, name, _format_for_toml(value), commented=False)
|
|
228
|
+
elif default is dataclasses.MISSING or default is None:
|
|
229
|
+
if is_ml:
|
|
230
|
+
_emit_assignment(lines, name, "'''\n'''", commented=True)
|
|
231
|
+
else:
|
|
232
|
+
lines.append(f"#{name} =")
|
|
233
|
+
else:
|
|
234
|
+
value = default
|
|
235
|
+
if is_ml and not isinstance(value, mlstr):
|
|
236
|
+
value = mlstr(value)
|
|
237
|
+
_emit_assignment(lines, name, _format_for_toml(value), commented=True)
|
|
238
|
+
|
|
239
|
+
return '\n'.join(lines) + '\n'
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def parse_toml_with_comments(toml_content: str) -> dict:
|
|
243
|
+
"""
|
|
244
|
+
Parse TOML content, returning only uncommented (user-modified) values.
|
|
245
|
+
|
|
246
|
+
Commented lines (``#key = value``) are standard TOML comments and are
|
|
247
|
+
excluded by the parser. Only actively set values are returned.
|
|
248
|
+
|
|
249
|
+
:param toml_content: Raw TOML file content.
|
|
250
|
+
:return: Dict of parsed key-value pairs.
|
|
251
|
+
"""
|
|
252
|
+
return tomllib.loads(toml_content)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def ensure_config(config_cls: type, config_path: Path) -> object:
|
|
256
|
+
"""
|
|
257
|
+
Main entry point. Call on every application run.
|
|
258
|
+
|
|
259
|
+
1. If the file does not exist, generate it with all defaults (commented).
|
|
260
|
+
2. If it exists, read user values, regenerate from the dataclass, write back.
|
|
261
|
+
3. Return a populated dataclass instance with user values over defaults.
|
|
262
|
+
|
|
263
|
+
The result is cached on ``config_cls._ensured``, so repeated calls
|
|
264
|
+
return the same instance without file I/O.
|
|
265
|
+
|
|
266
|
+
TOML table sections (e.g. ``[binance]``) not managed by the dataclass
|
|
267
|
+
are preserved verbatim at the end of the file.
|
|
268
|
+
|
|
269
|
+
:param config_cls: The config dataclass type (not an instance).
|
|
270
|
+
:param config_path: Path to the TOML file.
|
|
271
|
+
:return: A populated config dataclass instance.
|
|
272
|
+
"""
|
|
273
|
+
if hasattr(config_cls, '_ensured'):
|
|
274
|
+
return config_cls._ensured
|
|
275
|
+
|
|
276
|
+
user_values = None
|
|
277
|
+
extra_content = ""
|
|
278
|
+
|
|
279
|
+
if config_path.exists():
|
|
280
|
+
user_values, extra_content = _parse_existing(config_path, config_cls)
|
|
281
|
+
|
|
282
|
+
toml_content = generate_toml(config_cls, user_values)
|
|
283
|
+
|
|
284
|
+
if extra_content:
|
|
285
|
+
toml_content += '\n' + extra_content
|
|
286
|
+
if not extra_content.endswith('\n'):
|
|
287
|
+
toml_content += '\n'
|
|
288
|
+
|
|
289
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
290
|
+
config_path.write_text(toml_content, encoding='utf-8')
|
|
291
|
+
|
|
292
|
+
instance = _create_instance(config_cls, user_values)
|
|
293
|
+
config_cls._ensured = instance
|
|
294
|
+
return instance
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _parse_existing(config_path: Path, config_cls: type) -> tuple[dict, str]:
|
|
298
|
+
"""
|
|
299
|
+
Parse an existing config file to extract user values and extra sections.
|
|
300
|
+
|
|
301
|
+
:param config_path: Path to the TOML file.
|
|
302
|
+
:param config_cls: The config dataclass type.
|
|
303
|
+
:return: ``(user_values, extra_sections_raw_text)``.
|
|
304
|
+
"""
|
|
305
|
+
content = config_path.read_text(encoding='utf-8')
|
|
306
|
+
|
|
307
|
+
parsed = tomllib.loads(content)
|
|
308
|
+
|
|
309
|
+
field_names = {f.name for f in dataclasses.fields(cast(Any, config_cls))}
|
|
310
|
+
|
|
311
|
+
# Field-typed values are kept; non-field keys (typically TOML section
|
|
312
|
+
# headers like ``[binance]``) are left out and preserved verbatim via
|
|
313
|
+
# :func:`_extract_extra_sections`. Dict-typed user values (e.g.
|
|
314
|
+
# ``symbol_map = {...}``) are accepted only when the key matches a
|
|
315
|
+
# known dataclass field.
|
|
316
|
+
user_values: dict = {}
|
|
317
|
+
for key, value in parsed.items():
|
|
318
|
+
if key in field_names:
|
|
319
|
+
user_values[key] = value
|
|
320
|
+
|
|
321
|
+
extra_content = _extract_extra_sections(content, field_names)
|
|
322
|
+
|
|
323
|
+
return user_values, extra_content
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _extract_extra_sections(content: str, field_names: set[str] | None = None) -> str:
|
|
327
|
+
"""
|
|
328
|
+
Extract raw text of TOML table sections from file content.
|
|
329
|
+
|
|
330
|
+
Walks the file from top to bottom and keeps every ``[section]`` block
|
|
331
|
+
whose header does not match a known dataclass field. Field-matching
|
|
332
|
+
table sections (e.g. ``[symbol_map]``) are filtered out so the
|
|
333
|
+
self-healing writer does not re-emit them alongside the inline-table
|
|
334
|
+
form already produced by :func:`generate_toml`.
|
|
335
|
+
|
|
336
|
+
:param content: Raw file content.
|
|
337
|
+
:param field_names: Known dataclass field names whose sections should
|
|
338
|
+
be dropped from the extras. ``None`` keeps every
|
|
339
|
+
section (legacy behaviour).
|
|
340
|
+
:return: Raw text of extra sections, or empty string.
|
|
341
|
+
"""
|
|
342
|
+
lines = content.splitlines()
|
|
343
|
+
out: list[str] = []
|
|
344
|
+
skipping = False
|
|
345
|
+
for line in lines:
|
|
346
|
+
stripped = line.strip()
|
|
347
|
+
if stripped.startswith('[') and not stripped.startswith('#'):
|
|
348
|
+
# New section header — decide whether to keep or drop.
|
|
349
|
+
header = stripped.strip('[]').split('.', 1)[0]
|
|
350
|
+
skipping = field_names is not None and header in field_names
|
|
351
|
+
if not skipping:
|
|
352
|
+
out.append(line)
|
|
353
|
+
continue
|
|
354
|
+
if not skipping:
|
|
355
|
+
# Only start capturing once we've seen the first kept header.
|
|
356
|
+
if out:
|
|
357
|
+
out.append(line)
|
|
358
|
+
return '\n'.join(out)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _create_instance(config_cls: type, user_values: dict | None):
|
|
362
|
+
"""
|
|
363
|
+
Create a dataclass instance with user values merged over defaults.
|
|
364
|
+
|
|
365
|
+
Handles ``int`` to ``float`` coercion when the field default is a
|
|
366
|
+
float, and ``str`` to ``mlstr`` coercion when the field is annotated
|
|
367
|
+
as ``mlstr`` (tomllib returns plain ``str`` regardless).
|
|
368
|
+
|
|
369
|
+
:param config_cls: The config dataclass type.
|
|
370
|
+
:param user_values: User-modified values, or ``None``.
|
|
371
|
+
:return: A populated config dataclass instance.
|
|
372
|
+
"""
|
|
373
|
+
if not user_values:
|
|
374
|
+
return config_cls()
|
|
375
|
+
|
|
376
|
+
kwargs: dict = {}
|
|
377
|
+
for f in dataclasses.fields(cast(Any, config_cls)):
|
|
378
|
+
if f.name not in user_values:
|
|
379
|
+
continue
|
|
380
|
+
value = user_values[f.name]
|
|
381
|
+
if (
|
|
382
|
+
f.default is not dataclasses.MISSING
|
|
383
|
+
and isinstance(f.default, float)
|
|
384
|
+
and isinstance(value, int)
|
|
385
|
+
and not isinstance(value, bool)
|
|
386
|
+
):
|
|
387
|
+
value = float(value)
|
|
388
|
+
if _is_mlstr_field(f) and isinstance(value, str) and not isinstance(value, mlstr):
|
|
389
|
+
value = mlstr(value)
|
|
390
|
+
kwargs[f.name] = value
|
|
391
|
+
|
|
392
|
+
return config_cls(**kwargs)
|