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.
Files changed (261) hide show
  1. opencode_pyneruntime-6.6.4.dist-info/METADATA +281 -0
  2. opencode_pyneruntime-6.6.4.dist-info/RECORD +261 -0
  3. opencode_pyneruntime-6.6.4.dist-info/WHEEL +5 -0
  4. opencode_pyneruntime-6.6.4.dist-info/entry_points.txt +6 -0
  5. opencode_pyneruntime-6.6.4.dist-info/licenses/LICENSE +201 -0
  6. opencode_pyneruntime-6.6.4.dist-info/licenses/NOTICE +21 -0
  7. opencode_pyneruntime-6.6.4.dist-info/top_level.txt +1 -0
  8. pynecore/__init__.py +6 -0
  9. pynecore/cli/__init__.py +2 -0
  10. pynecore/cli/app.py +238 -0
  11. pynecore/cli/commands/__init__.py +343 -0
  12. pynecore/cli/commands/benchmark.py +186 -0
  13. pynecore/cli/commands/compile.py +198 -0
  14. pynecore/cli/commands/data.py +857 -0
  15. pynecore/cli/commands/debug.py +63 -0
  16. pynecore/cli/commands/optimize.py +956 -0
  17. pynecore/cli/commands/plugin.py +242 -0
  18. pynecore/cli/commands/run.py +2006 -0
  19. pynecore/cli/pluggable.py +132 -0
  20. pynecore/cli/utils/__init__.py +0 -0
  21. pynecore/cli/utils/api_error_handler.py +168 -0
  22. pynecore/cli/utils/broker_picker.py +330 -0
  23. pynecore/cli/utils/error_hook.py +28 -0
  24. pynecore/cli/utils/keyreader.py +178 -0
  25. pynecore/cli/utils/provider_picker.py +19 -0
  26. pynecore/cli/utils/symbol_browser.py +1149 -0
  27. pynecore/core/__init__.py +0 -0
  28. pynecore/core/aggregator.py +257 -0
  29. pynecore/core/bar_magnifier.py +168 -0
  30. pynecore/core/broker/__init__.py +64 -0
  31. pynecore/core/broker/defaults.py +113 -0
  32. pynecore/core/broker/disappearance.py +927 -0
  33. pynecore/core/broker/emulator.py +345 -0
  34. pynecore/core/broker/exceptions.py +346 -0
  35. pynecore/core/broker/idempotency.py +401 -0
  36. pynecore/core/broker/intent_builder.py +334 -0
  37. pynecore/core/broker/journal.py +1785 -0
  38. pynecore/core/broker/models.py +1600 -0
  39. pynecore/core/broker/native_failsafe_manager.py +1436 -0
  40. pynecore/core/broker/one_way_emulator.py +1128 -0
  41. pynecore/core/broker/position.py +787 -0
  42. pynecore/core/broker/run_identity.py +126 -0
  43. pynecore/core/broker/software_entry_stop_engine.py +351 -0
  44. pynecore/core/broker/software_partial_bracket_engine.py +1379 -0
  45. pynecore/core/broker/spot_inventory.py +1327 -0
  46. pynecore/core/broker/storage.py +2655 -0
  47. pynecore/core/broker/store_helpers.py +2161 -0
  48. pynecore/core/broker/sync_engine.py +16070 -0
  49. pynecore/core/broker/validation.py +382 -0
  50. pynecore/core/class_property.py +7 -0
  51. pynecore/core/config.py +392 -0
  52. pynecore/core/csv_file.py +547 -0
  53. pynecore/core/currency.py +262 -0
  54. pynecore/core/data_converter.py +1002 -0
  55. pynecore/core/datetime.py +296 -0
  56. pynecore/core/download_info.py +71 -0
  57. pynecore/core/download_runner.py +274 -0
  58. pynecore/core/htf_aggregator.py +181 -0
  59. pynecore/core/import_hook.py +358 -0
  60. pynecore/core/instance_state.py +494 -0
  61. pynecore/core/live_ltf_collector.py +442 -0
  62. pynecore/core/live_ltf_window.py +189 -0
  63. pynecore/core/live_runner.py +1347 -0
  64. pynecore/core/module_property.py +26 -0
  65. pynecore/core/ohlcv_file.py +1888 -0
  66. pynecore/core/overload.py +371 -0
  67. pynecore/core/pine_cast.py +113 -0
  68. pynecore/core/pine_export.py +95 -0
  69. pynecore/core/pine_method.py +244 -0
  70. pynecore/core/pine_range.py +86 -0
  71. pynecore/core/pine_udt.py +69 -0
  72. pynecore/core/plugin/__init__.py +394 -0
  73. pynecore/core/plugin/broker.py +781 -0
  74. pynecore/core/plugin/cli.py +96 -0
  75. pynecore/core/plugin/live_provider.py +208 -0
  76. pynecore/core/plugin/provider.py +331 -0
  77. pynecore/core/provider_string.py +148 -0
  78. pynecore/core/random.py +40 -0
  79. pynecore/core/resampler.py +686 -0
  80. pynecore/core/safe_convert.py +64 -0
  81. pynecore/core/script.py +1011 -0
  82. pynecore/core/script_runner.py +3202 -0
  83. pynecore/core/security.py +1749 -0
  84. pynecore/core/security_process.py +1253 -0
  85. pynecore/core/security_shm.py +456 -0
  86. pynecore/core/series.py +417 -0
  87. pynecore/core/strategy_stats.py +669 -0
  88. pynecore/core/symbol_map.py +134 -0
  89. pynecore/core/syminfo.py +505 -0
  90. pynecore/core/viz.py +591 -0
  91. pynecore/lib/__init__.py +1771 -0
  92. pynecore/lib/_fixnan.py +32 -0
  93. pynecore/lib/_math_stateful.py +202 -0
  94. pynecore/lib/_timeframe_change.py +101 -0
  95. pynecore/lib/adjustment.py +6 -0
  96. pynecore/lib/alert.py +39 -0
  97. pynecore/lib/alert.pyi +14 -0
  98. pynecore/lib/array.py +1051 -0
  99. pynecore/lib/barmerge.py +60 -0
  100. pynecore/lib/barstate.py +30 -0
  101. pynecore/lib/box.py +415 -0
  102. pynecore/lib/chart.py +128 -0
  103. pynecore/lib/color.py +152 -0
  104. pynecore/lib/color.pyi +50 -0
  105. pynecore/lib/currency.py +62 -0
  106. pynecore/lib/dayofweek.py +36 -0
  107. pynecore/lib/dayofweek.pyi +18 -0
  108. pynecore/lib/display.py +8 -0
  109. pynecore/lib/dividends.py +9 -0
  110. pynecore/lib/earnings.py +11 -0
  111. pynecore/lib/extend.py +6 -0
  112. pynecore/lib/font.py +5 -0
  113. pynecore/lib/footprint.py +79 -0
  114. pynecore/lib/format.py +11 -0
  115. pynecore/lib/hline.py +67 -0
  116. pynecore/lib/hline.pyi +24 -0
  117. pynecore/lib/label.py +409 -0
  118. pynecore/lib/line.py +433 -0
  119. pynecore/lib/linefill.py +93 -0
  120. pynecore/lib/location.py +11 -0
  121. pynecore/lib/log.py +362 -0
  122. pynecore/lib/map.py +150 -0
  123. pynecore/lib/math.py +385 -0
  124. pynecore/lib/matrix.py +708 -0
  125. pynecore/lib/order.py +8 -0
  126. pynecore/lib/pivotpointtype.py +8 -0
  127. pynecore/lib/plot.py +95 -0
  128. pynecore/lib/plot.pyi +33 -0
  129. pynecore/lib/polyline.py +91 -0
  130. pynecore/lib/position.py +15 -0
  131. pynecore/lib/request.py +281 -0
  132. pynecore/lib/runtime.py +5 -0
  133. pynecore/lib/scale.py +9 -0
  134. pynecore/lib/session.py +267 -0
  135. pynecore/lib/session.pyi +12 -0
  136. pynecore/lib/shape.py +18 -0
  137. pynecore/lib/size.py +12 -0
  138. pynecore/lib/splits.py +4 -0
  139. pynecore/lib/strategy/__init__.py +4778 -0
  140. pynecore/lib/strategy/closedtrades.py +347 -0
  141. pynecore/lib/strategy/closedtrades.pyi +53 -0
  142. pynecore/lib/strategy/commission.py +9 -0
  143. pynecore/lib/strategy/direction.py +9 -0
  144. pynecore/lib/strategy/oca.py +13 -0
  145. pynecore/lib/strategy/opentrades.py +281 -0
  146. pynecore/lib/strategy/opentrades.pyi +49 -0
  147. pynecore/lib/strategy/risk.py +109 -0
  148. pynecore/lib/string.py +649 -0
  149. pynecore/lib/syminfo.py +84 -0
  150. pynecore/lib/ta.py +2230 -0
  151. pynecore/lib/table.py +290 -0
  152. pynecore/lib/text.py +17 -0
  153. pynecore/lib/ticker.py +207 -0
  154. pynecore/lib/timeframe.py +293 -0
  155. pynecore/lib/volume_row.py +67 -0
  156. pynecore/lib/xloc.py +4 -0
  157. pynecore/lib/yloc.py +5 -0
  158. pynecore/providers/__init__.py +0 -0
  159. pynecore/providers/ccxt.py +664 -0
  160. pynecore/providers/replay.py +187 -0
  161. pynecore/pynesys/__init__.py +0 -0
  162. pynecore/pynesys/api.py +498 -0
  163. pynecore/pynesys/compiler.py +112 -0
  164. pynecore/standalone.py +99 -0
  165. pynecore/testing/__init__.py +1 -0
  166. pynecore/testing/broker_lab/__init__.py +41 -0
  167. pynecore/testing/broker_lab/__main__.py +5 -0
  168. pynecore/testing/broker_lab/cli.py +87 -0
  169. pynecore/testing/broker_lab/generate.py +47 -0
  170. pynecore/testing/broker_lab/model.py +84 -0
  171. pynecore/testing/broker_lab/reference.py +645 -0
  172. pynecore/testing/broker_lab/runner.py +372 -0
  173. pynecore/testing/broker_lab/scheduler.py +50 -0
  174. pynecore/testing/broker_lab/subprocess.py +73 -0
  175. pynecore/transformers/__init__.py +0 -0
  176. pynecore/transformers/builtin_shadow.py +136 -0
  177. pynecore/transformers/closure_arguments_transformer.py +428 -0
  178. pynecore/transformers/display_rewrite.py +140 -0
  179. pynecore/transformers/dynamic_default.py +147 -0
  180. pynecore/transformers/function_isolation.py +757 -0
  181. pynecore/transformers/import_lifter.py +61 -0
  182. pynecore/transformers/import_normalizer.py +328 -0
  183. pynecore/transformers/inline_series_hoist.py +178 -0
  184. pynecore/transformers/input_transformer.py +175 -0
  185. pynecore/transformers/lib_series.py +201 -0
  186. pynecore/transformers/locations.py +70 -0
  187. pynecore/transformers/module_properties.json +3387 -0
  188. pynecore/transformers/module_property.py +221 -0
  189. pynecore/transformers/ne_guard.py +70 -0
  190. pynecore/transformers/persistent.py +320 -0
  191. pynecore/transformers/persistent_series.py +76 -0
  192. pynecore/transformers/safe_convert_transformer.py +97 -0
  193. pynecore/transformers/safe_division_transformer.py +95 -0
  194. pynecore/transformers/script_requirements.py +308 -0
  195. pynecore/transformers/security.py +752 -0
  196. pynecore/transformers/security_instantiation.py +274 -0
  197. pynecore/transformers/series.py +275 -0
  198. pynecore/transformers/slot_layout.py +381 -0
  199. pynecore/transformers/type_checking_stripper.py +25 -0
  200. pynecore/transformers/unused_series_detector.py +267 -0
  201. pynecore/types/__init__.py +21 -0
  202. pynecore/types/alert.py +5 -0
  203. pynecore/types/barmerge.py +5 -0
  204. pynecore/types/base.py +39 -0
  205. pynecore/types/box.py +37 -0
  206. pynecore/types/chart.py +17 -0
  207. pynecore/types/color.py +107 -0
  208. pynecore/types/currency.py +5 -0
  209. pynecore/types/datetime.py +6 -0
  210. pynecore/types/display.py +5 -0
  211. pynecore/types/dividends.py +5 -0
  212. pynecore/types/earnings.py +5 -0
  213. pynecore/types/extend.py +5 -0
  214. pynecore/types/font.py +5 -0
  215. pynecore/types/footprint.py +41 -0
  216. pynecore/types/format.py +5 -0
  217. pynecore/types/hline.py +24 -0
  218. pynecore/types/ib_persistent.py +8 -0
  219. pynecore/types/ib_persistent.pyi +10 -0
  220. pynecore/types/label.py +35 -0
  221. pynecore/types/line.py +32 -0
  222. pynecore/types/linefill.py +13 -0
  223. pynecore/types/location.py +5 -0
  224. pynecore/types/matrix.py +999 -0
  225. pynecore/types/na.py +237 -0
  226. pynecore/types/na.pyi +83 -0
  227. pynecore/types/ohlcv.py +12 -0
  228. pynecore/types/order.py +5 -0
  229. pynecore/types/persistent.py +8 -0
  230. pynecore/types/persistent.pyi +13 -0
  231. pynecore/types/pine_types.py +11 -0
  232. pynecore/types/pine_types.pyi +15 -0
  233. pynecore/types/pivotpointtype.py +5 -0
  234. pynecore/types/plot.py +12 -0
  235. pynecore/types/plot_meta.py +60 -0
  236. pynecore/types/polyline.py +40 -0
  237. pynecore/types/position.py +5 -0
  238. pynecore/types/scale.py +5 -0
  239. pynecore/types/script_type.py +15 -0
  240. pynecore/types/series.py +23 -0
  241. pynecore/types/series.pyi +19 -0
  242. pynecore/types/session.py +35 -0
  243. pynecore/types/shape.py +5 -0
  244. pynecore/types/size.py +5 -0
  245. pynecore/types/source.py +33 -0
  246. pynecore/types/splits.py +5 -0
  247. pynecore/types/strategy.py +45 -0
  248. pynecore/types/table.py +87 -0
  249. pynecore/types/text.py +13 -0
  250. pynecore/types/type_checker.py +7 -0
  251. pynecore/types/type_checker.pyi +48 -0
  252. pynecore/types/volume_row.py +36 -0
  253. pynecore/types/weekdays.py +11 -0
  254. pynecore/types/xloc.py +5 -0
  255. pynecore/types/yloc.py +5 -0
  256. pynecore/utils/__init__.py +0 -0
  257. pynecore/utils/file_utils.py +50 -0
  258. pynecore/utils/rich/__init__.py +0 -0
  259. pynecore/utils/rich/date_column.py +25 -0
  260. pynecore/utils/sequence_view.py +92 -0
  261. 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