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
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Plugin base class, discovery, and loading via Python entry points.
|
|
3
|
+
|
|
4
|
+
All PyneCore plugins register under a single entry point group
|
|
5
|
+
(``pyne.plugin``) in their ``pyproject.toml``. The plugin class hierarchy
|
|
6
|
+
determines capabilities:
|
|
7
|
+
|
|
8
|
+
- ``ProviderPlugin(Plugin)`` — offline OHLCV data provider
|
|
9
|
+
- ``LiveProviderPlugin(ProviderPlugin)`` — offline + WebSocket/streaming data
|
|
10
|
+
- ``CLIPlugin(Plugin)`` — CLI commands and parameter hooks
|
|
11
|
+
|
|
12
|
+
Multiple inheritance combines capabilities::
|
|
13
|
+
|
|
14
|
+
class YahooPlugin(ProviderPlugin, CLIPlugin): ... # offline only
|
|
15
|
+
class BinancePlugin(LiveProviderPlugin, CLIPlugin): ... # offline + live
|
|
16
|
+
|
|
17
|
+
Plugin metadata (name, version) comes from the package's ``pyproject.toml``
|
|
18
|
+
via :mod:`importlib.metadata`, not from class attributes.
|
|
19
|
+
|
|
20
|
+
Example ``pyproject.toml``::
|
|
21
|
+
|
|
22
|
+
[project.entry-points."pyne.plugin"]
|
|
23
|
+
myexchange = "mypackage:MyExchangeProvider"
|
|
24
|
+
|
|
25
|
+
Discovery::
|
|
26
|
+
|
|
27
|
+
plugins = discover_plugins()
|
|
28
|
+
cls = load_plugin("capitalcom")
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
import inspect
|
|
32
|
+
import re
|
|
33
|
+
import sys
|
|
34
|
+
from collections.abc import Iterable
|
|
35
|
+
from typing import TypeVar, Generic, Any
|
|
36
|
+
|
|
37
|
+
# noinspection PyProtectedMember
|
|
38
|
+
from importlib.metadata import entry_points, EntryPoint
|
|
39
|
+
|
|
40
|
+
if sys.version_info >= (3, 12):
|
|
41
|
+
# noinspection PyUnusedImports
|
|
42
|
+
from typing import override
|
|
43
|
+
else:
|
|
44
|
+
def override(func):
|
|
45
|
+
"""Marks a method as overriding a base class method (polyfill for <3.12)."""
|
|
46
|
+
return func
|
|
47
|
+
|
|
48
|
+
PLUGIN_GROUP = 'pyne.plugin'
|
|
49
|
+
|
|
50
|
+
ConfigT = TypeVar('ConfigT')
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class Plugin(Generic[ConfigT]):
|
|
54
|
+
"""
|
|
55
|
+
Minimal base class for all PyneCore plugins.
|
|
56
|
+
|
|
57
|
+
Generic over the config dataclass type. Plugin authors should inherit
|
|
58
|
+
from a concrete subclass: :class:`ProviderPlugin`, :class:`LiveProviderPlugin`,
|
|
59
|
+
:class:`~pynecore.core.plugin.broker.BrokerPlugin`, :class:`CLIPlugin`,
|
|
60
|
+
or a combination via multiple inheritance.
|
|
61
|
+
|
|
62
|
+
Example::
|
|
63
|
+
|
|
64
|
+
class MyProvider(ProviderPlugin[MyConfig]):
|
|
65
|
+
Config = MyConfig
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
Config: type[ConfigT] | None = None
|
|
69
|
+
"""Override with a ``@dataclass`` for plugin configuration."""
|
|
70
|
+
|
|
71
|
+
plugin_name: str = ""
|
|
72
|
+
"""Optional display name override. If empty, the entry point name is used."""
|
|
73
|
+
|
|
74
|
+
plugin_params: dict[str, Any] = {}
|
|
75
|
+
"""Values of plugin-injected CLI flags for the running command, populated by
|
|
76
|
+
the core CLI before invoking the plugin (see :class:`CLIPlugin.cli_params`).
|
|
77
|
+
Each plugin reads only its own keys; empty when no command set it."""
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class PluginNotFoundError(ImportError):
|
|
81
|
+
"""Raised when a requested plugin is not installed."""
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class PluginNameConflictError(ImportError):
|
|
85
|
+
"""Raised when the requested plugin name is provided by several packages.
|
|
86
|
+
|
|
87
|
+
Entry point names are not unique — any package may register the same
|
|
88
|
+
``pyne.plugin`` name — so loading one of them by guesswork could run
|
|
89
|
+
foreign code under a trusted name.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class ProviderError(Exception):
|
|
94
|
+
"""Base class for *expected*, user-actionable data-provider failures.
|
|
95
|
+
|
|
96
|
+
Covers conditions a user can act on — missing/invalid credentials, a
|
|
97
|
+
connection that could not be opened, an unknown broker — as opposed to
|
|
98
|
+
programming errors. The ``pyne data`` CLI catches this (alongside
|
|
99
|
+
:class:`NotImplementedError`) on its listing/download paths and prints a
|
|
100
|
+
one-line ``Error: ...`` instead of a traceback. A plugin's own error
|
|
101
|
+
hierarchy should subclass it so those failures surface cleanly.
|
|
102
|
+
|
|
103
|
+
:cvar retryable: Whether the same operation could plausibly succeed if
|
|
104
|
+
retried later — a *transient* connectivity / broker-unavailable fault,
|
|
105
|
+
as opposed to a permanent misconfiguration (unknown symbol, bad
|
|
106
|
+
credentials, wrong account mode). Defaults to ``False`` so an unknown
|
|
107
|
+
failure fails fast rather than looping forever; genuine transient
|
|
108
|
+
faults set it via :class:`TransientProviderError` or a ``retryable``
|
|
109
|
+
property keyed on an error code.
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
retryable: bool = False
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class TransientProviderError(ProviderError):
|
|
116
|
+
"""A *transient* provider failure — the operation may succeed on retry.
|
|
117
|
+
|
|
118
|
+
Connectivity drops, request-routing failures (broker maintenance), socket
|
|
119
|
+
timeouts. A long-running ``--broker`` / ``--live`` run should wait and
|
|
120
|
+
retry rather than halt; a one-shot backtest still fails fast. See
|
|
121
|
+
:func:`is_retryable_provider_error`.
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
retryable: bool = True
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def is_retryable_provider_error(exc: BaseException) -> bool:
|
|
128
|
+
"""Whether ``exc`` is a transient, retry-worthy provider failure.
|
|
129
|
+
|
|
130
|
+
Walks the ``__cause__`` / ``__context__`` chain so a transient error stays
|
|
131
|
+
classifiable even after being re-wrapped in another exception (the
|
|
132
|
+
original wire fault is often nested under a higher-level
|
|
133
|
+
:class:`ProviderError`). Returns ``True`` as soon as any link is a
|
|
134
|
+
:class:`ProviderError` whose ``retryable`` flag is set.
|
|
135
|
+
|
|
136
|
+
:param exc: The caught exception.
|
|
137
|
+
:return: ``True`` if waiting and retrying the operation could plausibly
|
|
138
|
+
succeed, ``False`` for permanent / unknown failures.
|
|
139
|
+
"""
|
|
140
|
+
seen: set[int] = set()
|
|
141
|
+
current: BaseException | None = exc
|
|
142
|
+
while current is not None and id(current) not in seen:
|
|
143
|
+
seen.add(id(current))
|
|
144
|
+
if isinstance(current, ProviderError) and current.retryable:
|
|
145
|
+
return True
|
|
146
|
+
current = current.__cause__ or current.__context__
|
|
147
|
+
return False
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def get_plugin_package(ep: EntryPoint) -> str:
|
|
151
|
+
"""
|
|
152
|
+
Return the distribution (PyPI package) name providing an entry point.
|
|
153
|
+
|
|
154
|
+
:param ep: The entry point of the plugin.
|
|
155
|
+
:return: Package name, or ``"(unknown package)"`` if the distribution is
|
|
156
|
+
not resolvable.
|
|
157
|
+
"""
|
|
158
|
+
if ep.dist is None:
|
|
159
|
+
return '(unknown package)'
|
|
160
|
+
return ep.dist.metadata['Name'] or ep.dist.name or '(unknown package)'
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def discover_plugin_entry_points() -> dict[str, list[EntryPoint]]:
|
|
164
|
+
"""
|
|
165
|
+
Return every installed plugin entry point, grouped by plugin name.
|
|
166
|
+
|
|
167
|
+
Entry point names are **not** unique: any package may register e.g.
|
|
168
|
+
``bybit`` under the ``pyne.plugin`` group, so a name may map to several
|
|
169
|
+
entry points. Conflicts are reported here, never resolved silently.
|
|
170
|
+
|
|
171
|
+
:return: Mapping of plugin name to all entry points declaring it, each
|
|
172
|
+
list ordered deterministically by providing package.
|
|
173
|
+
"""
|
|
174
|
+
grouped: dict[str, list[EntryPoint]] = {}
|
|
175
|
+
for ep in entry_points(group=PLUGIN_GROUP):
|
|
176
|
+
grouped.setdefault(ep.name, []).append(ep)
|
|
177
|
+
for eps in grouped.values():
|
|
178
|
+
eps.sort(key=lambda ep: (get_plugin_package(ep), ep.value))
|
|
179
|
+
return grouped
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def discover_plugins() -> dict[str, EntryPoint]:
|
|
183
|
+
"""
|
|
184
|
+
Return all installed plugins, one entry point per name.
|
|
185
|
+
|
|
186
|
+
On a name conflict a deterministic winner is picked (first by providing
|
|
187
|
+
package name) so listings stay stable. This function never raises on a
|
|
188
|
+
conflict on purpose: two unrelated third-party plugins colliding must not
|
|
189
|
+
take down every plugin lookup — that would halt a running bot. Only
|
|
190
|
+
:func:`load_plugin` refuses to guess, and only for the requested name.
|
|
191
|
+
|
|
192
|
+
:return: Mapping of plugin name to a single :class:`EntryPoint`.
|
|
193
|
+
"""
|
|
194
|
+
return {name: eps[0] for name, eps in discover_plugin_entry_points().items()}
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def load_plugin(name: str) -> type:
|
|
198
|
+
"""
|
|
199
|
+
Load and return a plugin class by name.
|
|
200
|
+
|
|
201
|
+
:param name: Plugin name as declared in the entry point.
|
|
202
|
+
:return: The plugin class.
|
|
203
|
+
:raises PluginNotFoundError: If no plugin with the given name is installed.
|
|
204
|
+
:raises PluginNameConflictError: If several installed packages declare the
|
|
205
|
+
same plugin name — loading either one would be a guess.
|
|
206
|
+
"""
|
|
207
|
+
grouped = discover_plugin_entry_points()
|
|
208
|
+
candidates = grouped.get(name)
|
|
209
|
+
if not candidates:
|
|
210
|
+
raise PluginNotFoundError(
|
|
211
|
+
f"Plugin '{name}' is not installed.\n"
|
|
212
|
+
f"Available plugins: {', '.join(sorted(grouped)) or '(none)'}"
|
|
213
|
+
)
|
|
214
|
+
if len(candidates) > 1:
|
|
215
|
+
packages = ', '.join(get_plugin_package(ep) for ep in candidates)
|
|
216
|
+
raise PluginNameConflictError(
|
|
217
|
+
f"Plugin name '{name}' is ambiguous — declared by {len(candidates)} "
|
|
218
|
+
f"installed packages: {packages}\n"
|
|
219
|
+
f"Uninstall all but the one you want to use."
|
|
220
|
+
)
|
|
221
|
+
return candidates[0].load()
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def get_available_plugin_names() -> list[str]:
|
|
225
|
+
"""
|
|
226
|
+
Return a sorted list of all available plugin names.
|
|
227
|
+
|
|
228
|
+
:return: Sorted list of plugin names.
|
|
229
|
+
"""
|
|
230
|
+
return sorted(discover_plugins())
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def get_plugin_metadata(ep: EntryPoint) -> dict[str, str]:
|
|
234
|
+
"""
|
|
235
|
+
Extract plugin metadata from its package distribution.
|
|
236
|
+
|
|
237
|
+
:param ep: The entry point of the plugin.
|
|
238
|
+
:return: Dict with ``name``, ``package``, ``version``, ``description``,
|
|
239
|
+
``requires_pynecore``, ``min_pynecore``.
|
|
240
|
+
"""
|
|
241
|
+
assert ep.dist is not None
|
|
242
|
+
meta = ep.dist.metadata
|
|
243
|
+
specifier = parse_pynecore_requirement(ep)
|
|
244
|
+
return {
|
|
245
|
+
'name': ep.name,
|
|
246
|
+
'package': get_plugin_package(ep),
|
|
247
|
+
'version': meta['Version'] or '',
|
|
248
|
+
'description': meta['Summary'] or '',
|
|
249
|
+
'requires_pynecore': specifier,
|
|
250
|
+
'min_pynecore': min_pynecore_version(specifier),
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def get_plugin_summary(cls: type) -> str:
|
|
255
|
+
"""
|
|
256
|
+
Return the first paragraph of the plugin class docstring.
|
|
257
|
+
|
|
258
|
+
The first paragraph is the text up to the first blank line, with
|
|
259
|
+
internal newlines collapsed to single spaces — suitable for a
|
|
260
|
+
one-line summary in listings.
|
|
261
|
+
|
|
262
|
+
:param cls: The plugin class.
|
|
263
|
+
:return: First-paragraph summary, or ``""`` if no docstring.
|
|
264
|
+
"""
|
|
265
|
+
doc = inspect.getdoc(cls) or ""
|
|
266
|
+
first_para = doc.split("\n\n", 1)[0].strip()
|
|
267
|
+
return " ".join(first_para.split())
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def get_plugin_description(cls: type) -> str:
|
|
271
|
+
"""
|
|
272
|
+
Return the full normalized plugin class docstring.
|
|
273
|
+
|
|
274
|
+
Uses :func:`inspect.getdoc`, which strips the uniform leading
|
|
275
|
+
indentation per PEP 257.
|
|
276
|
+
|
|
277
|
+
:param cls: The plugin class.
|
|
278
|
+
:return: Normalized docstring, or ``""`` if no docstring.
|
|
279
|
+
"""
|
|
280
|
+
return inspect.getdoc(cls) or ""
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
PYNECORE_PACKAGE = 'pynesys-pynecore'
|
|
284
|
+
|
|
285
|
+
_REQUIREMENT_RE = re.compile(
|
|
286
|
+
r'^\s*(?P<name>[A-Za-z0-9][A-Za-z0-9._-]*)' # PEP 508 distribution name
|
|
287
|
+
r'\s*(?:\[(?P<extras>[^]]*)])?' # optional extras
|
|
288
|
+
r'\s*(?:\((?P<paren>[^)]*)\)|(?P<bare>[^;]*))?' # specifier, optionally parenthesized
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
_SPECIFIER_RE = re.compile(r'^\s*(?P<op>===|~=|==|!=|<=|>=|<|>)\s*(?P<version>\S+?)\s*$')
|
|
292
|
+
|
|
293
|
+
# Operators that place a *lower* bound on the version
|
|
294
|
+
_LOWER_BOUND_OPS = frozenset(('>=', '==', '===', '~='))
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def normalize_package_name(name: str) -> str:
|
|
298
|
+
"""
|
|
299
|
+
Normalize a distribution name for comparison (PEP 503).
|
|
300
|
+
|
|
301
|
+
``PyneSys_PyneCore`` and ``pynesys-pynecore`` are the same project.
|
|
302
|
+
|
|
303
|
+
:param name: Raw distribution name.
|
|
304
|
+
:return: Lowercase name with runs of ``-``, ``_``, ``.`` collapsed to ``-``.
|
|
305
|
+
"""
|
|
306
|
+
return re.sub(r'[-_.]+', '-', name).strip().lower()
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def parse_pynecore_requires(requires: Iterable[str]) -> str:
|
|
310
|
+
"""
|
|
311
|
+
Return the PyneCore version constraint declared in a ``Requires-Dist`` list.
|
|
312
|
+
|
|
313
|
+
Returns the whole specifier set of the :data:`PYNECORE_PACKAGE` requirement
|
|
314
|
+
— ``">=6.6.0"``, ``">=6.6.0,<7"``, ``"~=6.6"`` — not just a lower bound,
|
|
315
|
+
since an upper bound is meaningful: PyneCore versions are
|
|
316
|
+
``PineVersion.Major.Minor``, so ``<7`` means "Pine v6 only". Extras,
|
|
317
|
+
parenthesized specifiers, environment markers, arbitrary whitespace and
|
|
318
|
+
non-normalized package names are all handled.
|
|
319
|
+
|
|
320
|
+
Takes the raw requirement strings rather than a distribution, so it also
|
|
321
|
+
works on metadata read from outside the running environment (e.g. the PyPI
|
|
322
|
+
JSON API's ``requires_dist``).
|
|
323
|
+
|
|
324
|
+
:param requires: ``Requires-Dist`` values of a distribution.
|
|
325
|
+
:return: Normalized specifier set (e.g. ``">=6.6.0,<7"``), or ``""`` if the
|
|
326
|
+
package does not declare a PyneCore dependency.
|
|
327
|
+
"""
|
|
328
|
+
for req in requires:
|
|
329
|
+
m = _REQUIREMENT_RE.match(req.split(';', 1)[0])
|
|
330
|
+
if m is None or normalize_package_name(m.group('name')) != PYNECORE_PACKAGE:
|
|
331
|
+
continue
|
|
332
|
+
raw = m.group('paren') or m.group('bare') or ''
|
|
333
|
+
clauses = (_SPECIFIER_RE.match(c) for c in raw.split(',') if c.strip())
|
|
334
|
+
return ','.join(f"{c.group('op')}{c.group('version')}" for c in clauses if c)
|
|
335
|
+
return ''
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def parse_pynecore_requirement(ep: EntryPoint) -> str:
|
|
339
|
+
"""
|
|
340
|
+
Return the plugin's declared PyneCore version constraint.
|
|
341
|
+
|
|
342
|
+
:param ep: The entry point of the plugin.
|
|
343
|
+
:return: Normalized specifier set (e.g. ``">=6.6.0,<7"``), or ``""`` if the
|
|
344
|
+
package does not declare a PyneCore dependency.
|
|
345
|
+
"""
|
|
346
|
+
assert ep.dist is not None
|
|
347
|
+
return parse_pynecore_requires(ep.dist.requires or ())
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def min_pynecore_version(specifier: str) -> str:
|
|
351
|
+
"""
|
|
352
|
+
Return the lowest PyneCore version allowed by a specifier set.
|
|
353
|
+
|
|
354
|
+
Of the lower-bounding clauses (``>=``, ``==``, ``===``, ``~=``) the highest
|
|
355
|
+
version wins; upper bounds and exclusions are ignored. Versions are
|
|
356
|
+
compared component-wise on their numeric release parts, so ``6.10.0`` is
|
|
357
|
+
correctly greater than ``6.9.0`` — a plain string compare is wrong here.
|
|
358
|
+
|
|
359
|
+
:param specifier: Specifier set as returned by
|
|
360
|
+
:func:`parse_pynecore_requirement`.
|
|
361
|
+
:return: Version string (e.g. ``"6.6.0"``), or ``""`` if the specifier set
|
|
362
|
+
has no lower bound.
|
|
363
|
+
"""
|
|
364
|
+
best = ''
|
|
365
|
+
for clause in specifier.split(','):
|
|
366
|
+
m = _SPECIFIER_RE.match(clause)
|
|
367
|
+
if m is None or m.group('op') not in _LOWER_BOUND_OPS:
|
|
368
|
+
continue
|
|
369
|
+
version = m.group('version').rstrip('*').rstrip('.')
|
|
370
|
+
if not version:
|
|
371
|
+
continue
|
|
372
|
+
if not best or _version_key(version) > _version_key(best):
|
|
373
|
+
best = version
|
|
374
|
+
return best
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def _version_key(version: str) -> tuple[int, ...]:
|
|
378
|
+
"""
|
|
379
|
+
Return a comparable key for the numeric release part of a version.
|
|
380
|
+
|
|
381
|
+
Pre/post/local suffixes (``6.6.0rc1``, ``6.6.0+local``) are cut off — this
|
|
382
|
+
is used for ordering lower bounds, not for full PEP 440 semantics.
|
|
383
|
+
|
|
384
|
+
:param version: Version string.
|
|
385
|
+
:return: Tuple of the leading integer components.
|
|
386
|
+
"""
|
|
387
|
+
release = re.match(r'\d+(?:\.\d+)*', version)
|
|
388
|
+
return tuple(int(p) for p in release.group(0).split('.')) if release else ()
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
# Plugin type subclasses — import after Plugin is defined to avoid circular imports
|
|
392
|
+
from .provider import ProviderPlugin, Broker
|
|
393
|
+
from .live_provider import LiveProviderPlugin, LiveProviderConfig, PluginSymbol
|
|
394
|
+
from .cli import CLIPlugin, CLIOption
|