scikit-view 0.0.4__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.
- scikit_view-0.0.4/LICENSE +21 -0
- scikit_view-0.0.4/PKG-INFO +28 -0
- scikit_view-0.0.4/README.md +2 -0
- scikit_view-0.0.4/pyproject.toml +44 -0
- scikit_view-0.0.4/scikit_view/__init__.py +26 -0
- scikit_view-0.0.4/scikit_view/__main__.py +11 -0
- scikit_view-0.0.4/scikit_view/api.py +117 -0
- scikit_view-0.0.4/scikit_view/cli.py +66 -0
- scikit_view-0.0.4/scikit_view/registry.py +88 -0
- scikit_view-0.0.4/scikit_view/types.py +64 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 兮尘
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: scikit-view
|
|
3
|
+
Version: 0.0.4
|
|
4
|
+
Summary: science viewer kit for coding agents
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Keywords: agent,framework,AI,tool,skill,plugin,toolkit,library,python
|
|
8
|
+
Author: Xueyuan Lin
|
|
9
|
+
Author-email: linxy59@mail2.sysu.edu.cn
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Requires-Dist: click (>=8.1.0)
|
|
20
|
+
Requires-Dist: loguru (>=0.7.0)
|
|
21
|
+
Requires-Dist: stevedore (>=5.0.0)
|
|
22
|
+
Project-URL: Homepage, https://linxueyuan.online/scikit-view
|
|
23
|
+
Project-URL: Repository, https://github.com/LinXueyuanStdio/scikit-view
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# scikit
|
|
27
|
+
universal science kit for coding agents
|
|
28
|
+
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["poetry-core>=1.9.0"]
|
|
3
|
+
build-backend = "poetry.core.masonry.api"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "scikit-view"
|
|
7
|
+
version = "0.0.4"
|
|
8
|
+
description = "science viewer kit for coding agents"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [
|
|
14
|
+
{ name = "Xueyuan Lin", email = "linxy59@mail2.sysu.edu.cn" },
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[tool.poetry]
|
|
18
|
+
name = "scikit-view"
|
|
19
|
+
version = "0.0.4"
|
|
20
|
+
description = "science viewer kit for coding agents"
|
|
21
|
+
authors = ["Xueyuan Lin <linxy59@mail2.sysu.edu.cn>"]
|
|
22
|
+
packages = [{ include = "scikit_view" }]
|
|
23
|
+
homepage = "https://linxueyuan.online/scikit-view"
|
|
24
|
+
repository = "https://github.com/LinXueyuanStdio/scikit-view"
|
|
25
|
+
keywords = ["agent", "framework", "AI", "tool", "skill", "plugin", "toolkit", "library", "python"]
|
|
26
|
+
classifiers = [
|
|
27
|
+
"Programming Language :: Python :: 3.10",
|
|
28
|
+
"Programming Language :: Python :: 3.11",
|
|
29
|
+
"Programming Language :: Python :: 3.12",
|
|
30
|
+
"Programming Language :: Python :: 3.13",
|
|
31
|
+
"Programming Language :: Python :: 3.14",
|
|
32
|
+
"License :: OSI Approved :: MIT License",
|
|
33
|
+
"Operating System :: OS Independent",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
[tool.poetry.scripts]
|
|
37
|
+
scikit-view = "scikit_view.__main__:main"
|
|
38
|
+
sv = "scikit_view.__main__:main"
|
|
39
|
+
|
|
40
|
+
[tool.poetry.dependencies]
|
|
41
|
+
|
|
42
|
+
stevedore = ">=5.0.0"
|
|
43
|
+
click = ">=8.1.0"
|
|
44
|
+
loguru = ">=0.7.0"
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""scikit — produce agent-readable views of scientific files.
|
|
2
|
+
|
|
3
|
+
The public surface is deliberately small: a :func:`view` entry point, the
|
|
4
|
+
:class:`Viewer` contract plugins implement, the :class:`ViewResult` returned
|
|
5
|
+
on success, and the exception types the call can raise.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .api import view
|
|
9
|
+
from .types import (
|
|
10
|
+
NoViewerFound,
|
|
11
|
+
OutputDirNotEmpty,
|
|
12
|
+
ScikitError,
|
|
13
|
+
ViewFailed,
|
|
14
|
+
Viewer,
|
|
15
|
+
ViewResult,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"view",
|
|
20
|
+
"Viewer",
|
|
21
|
+
"ViewResult",
|
|
22
|
+
"ScikitError",
|
|
23
|
+
"NoViewerFound",
|
|
24
|
+
"ViewFailed",
|
|
25
|
+
"OutputDirNotEmpty",
|
|
26
|
+
]
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""The public ``view()`` entry point and its output-directory handling."""
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from .registry import Registry
|
|
8
|
+
from .types import OutputDirNotEmpty, ScikitError, ViewFailed, Viewer, ViewResult
|
|
9
|
+
|
|
10
|
+
DEFAULT_ROOT = Path(".scikit") / "files"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def default_output_dir(source: Path) -> Path:
|
|
14
|
+
"""The default output directory for ``source``: ``./.scikit/files/<name>/``."""
|
|
15
|
+
name = source.name
|
|
16
|
+
if not name:
|
|
17
|
+
raise ScikitError(
|
|
18
|
+
f"Cannot derive an output directory for {str(source)!r}: it has no file name"
|
|
19
|
+
)
|
|
20
|
+
return DEFAULT_ROOT / name
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _clear_dir(path: Path) -> None:
|
|
24
|
+
"""Remove ``path``, never following a symlink to its target."""
|
|
25
|
+
if path.is_symlink() or path.is_file():
|
|
26
|
+
path.unlink()
|
|
27
|
+
elif path.is_dir():
|
|
28
|
+
shutil.rmtree(path)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _is_empty(path: Path) -> bool:
|
|
32
|
+
return path.is_dir() and not any(path.iterdir())
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _is_within(path: Path, root: Path) -> bool:
|
|
36
|
+
return path.resolve().is_relative_to(root.resolve())
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def resolve_output_dir(
|
|
40
|
+
source: Path,
|
|
41
|
+
output_dir: Optional[Path],
|
|
42
|
+
force: bool,
|
|
43
|
+
) -> Path:
|
|
44
|
+
"""Resolve the output directory, applying the rebuild/overwrite rules.
|
|
45
|
+
|
|
46
|
+
scikit-owned directories (under ``.scikit/files/``) are rebuilt freely;
|
|
47
|
+
user-specified directories are only overwritten with ``force``. Clearing a
|
|
48
|
+
scikit-owned directory that resolves outside ``.scikit/files/`` (e.g. via a
|
|
49
|
+
symlink) is refused.
|
|
50
|
+
"""
|
|
51
|
+
source = Path(source)
|
|
52
|
+
|
|
53
|
+
if output_dir is None:
|
|
54
|
+
outdir = default_output_dir(source)
|
|
55
|
+
if outdir.exists():
|
|
56
|
+
if not _is_within(outdir, DEFAULT_ROOT):
|
|
57
|
+
raise ScikitError(
|
|
58
|
+
f"Refusing to clear {outdir}: it resolves outside {DEFAULT_ROOT}"
|
|
59
|
+
)
|
|
60
|
+
_clear_dir(outdir)
|
|
61
|
+
outdir.mkdir(parents=True, exist_ok=True)
|
|
62
|
+
return outdir
|
|
63
|
+
|
|
64
|
+
outdir = Path(output_dir)
|
|
65
|
+
if outdir.exists() and not _is_empty(outdir):
|
|
66
|
+
if not force:
|
|
67
|
+
raise OutputDirNotEmpty(
|
|
68
|
+
f"Output directory {outdir} is not empty; pass force=True to overwrite"
|
|
69
|
+
)
|
|
70
|
+
_clear_dir(outdir)
|
|
71
|
+
outdir.mkdir(parents=True, exist_ok=True)
|
|
72
|
+
return outdir
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def build_view(source: Path, output_dir: Path, viewer_obj: Viewer) -> ViewResult:
|
|
76
|
+
"""Run ``viewer_obj``, write its markdown to ``index.md`` and collect artifacts."""
|
|
77
|
+
source = Path(source)
|
|
78
|
+
output_dir = Path(output_dir)
|
|
79
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
markdown = viewer_obj.view(source, output_dir)
|
|
83
|
+
except Exception as exc:
|
|
84
|
+
raise ViewFailed(f"viewer {viewer_obj.name!r} failed on {str(source)!r}") from exc
|
|
85
|
+
|
|
86
|
+
markdown_path = output_dir / "index.md"
|
|
87
|
+
markdown_path.write_text(markdown, encoding="utf-8")
|
|
88
|
+
|
|
89
|
+
artifacts = tuple(sorted(p for p in output_dir.rglob("*") if p.is_file()))
|
|
90
|
+
return ViewResult(
|
|
91
|
+
source=source,
|
|
92
|
+
output_dir=output_dir,
|
|
93
|
+
viewer=viewer_obj.name,
|
|
94
|
+
markdown_path=markdown_path,
|
|
95
|
+
artifacts=artifacts,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def view(
|
|
100
|
+
source,
|
|
101
|
+
output_dir=None,
|
|
102
|
+
*,
|
|
103
|
+
viewer: Optional[str] = None,
|
|
104
|
+
force: bool = False,
|
|
105
|
+
registry: Optional[Registry] = None,
|
|
106
|
+
) -> ViewResult:
|
|
107
|
+
"""Produce an agent-readable view of ``source``.
|
|
108
|
+
|
|
109
|
+
Selects a viewer (routing, or the explicit ``viewer`` name), resolves the
|
|
110
|
+
output directory, runs the viewer and returns a :class:`ViewResult`.
|
|
111
|
+
"""
|
|
112
|
+
if registry is None:
|
|
113
|
+
registry = Registry()
|
|
114
|
+
source = Path(source)
|
|
115
|
+
viewer_obj = registry.select(source, requested=viewer)
|
|
116
|
+
resolved = resolve_output_dir(source, output_dir, force)
|
|
117
|
+
return build_view(source, resolved, viewer_obj)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""The ``scikit`` command-line interface."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
from .api import view as _run_view
|
|
10
|
+
from .types import NoViewerFound, OutputDirNotEmpty, ScikitError, ViewFailed, ViewResult
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _fail(exc: ScikitError, code: int) -> None:
|
|
14
|
+
click.echo(str(exc), err=True)
|
|
15
|
+
raise click.exceptions.Exit(code)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _result_to_dict(result: ViewResult) -> dict:
|
|
19
|
+
return {
|
|
20
|
+
"source": str(result.source),
|
|
21
|
+
"output_dir": str(result.output_dir),
|
|
22
|
+
"viewer": result.viewer,
|
|
23
|
+
"markdown_path": str(result.markdown_path),
|
|
24
|
+
"artifacts": [str(a) for a in result.artifacts],
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@click.group(name="scikit")
|
|
29
|
+
def cli() -> None:
|
|
30
|
+
"""Produce agent-readable views of scientific files."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@cli.command()
|
|
34
|
+
@click.argument("source", type=click.Path(path_type=Path))
|
|
35
|
+
@click.option("-o", "--output-dir", type=click.Path(path_type=Path), default=None)
|
|
36
|
+
@click.option("--viewer", default=None)
|
|
37
|
+
@click.option("--force", is_flag=True, default=False)
|
|
38
|
+
@click.option("--json", "as_json", is_flag=True, default=False)
|
|
39
|
+
def view(
|
|
40
|
+
source: Path,
|
|
41
|
+
output_dir: Optional[Path],
|
|
42
|
+
viewer: Optional[str],
|
|
43
|
+
force: bool,
|
|
44
|
+
as_json: bool,
|
|
45
|
+
) -> None:
|
|
46
|
+
"""Produce an agent-readable view of SOURCE (markdown on stdout by default)."""
|
|
47
|
+
try:
|
|
48
|
+
result = _run_view(source, output_dir=output_dir, viewer=viewer, force=force)
|
|
49
|
+
except NoViewerFound as exc:
|
|
50
|
+
_fail(exc, 3)
|
|
51
|
+
except OutputDirNotEmpty as exc:
|
|
52
|
+
_fail(exc, 4)
|
|
53
|
+
except ViewFailed as exc:
|
|
54
|
+
_fail(exc, 1)
|
|
55
|
+
except ScikitError as exc:
|
|
56
|
+
_fail(exc, 1)
|
|
57
|
+
|
|
58
|
+
if as_json:
|
|
59
|
+
click.echo(json.dumps(_result_to_dict(result), indent=2))
|
|
60
|
+
else:
|
|
61
|
+
click.echo(result.markdown_path.read_text(encoding="utf-8"))
|
|
62
|
+
|
|
63
|
+
click.echo(
|
|
64
|
+
f"viewer={result.viewer} artifacts={len(result.artifacts)} output={result.output_dir}",
|
|
65
|
+
err=True,
|
|
66
|
+
)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Viewer discovery and routing.
|
|
2
|
+
|
|
3
|
+
Discovery is delegated to stevedore (setuptools entry points under the
|
|
4
|
+
``scikit.viewers`` namespace). Routing is a pure function so it can be tested
|
|
5
|
+
without a real entry-point environment.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Iterable, Optional
|
|
10
|
+
|
|
11
|
+
from loguru import logger
|
|
12
|
+
from stevedore.extension import ExtensionManager
|
|
13
|
+
|
|
14
|
+
from .types import NoViewerFound, Viewer
|
|
15
|
+
|
|
16
|
+
NAMESPACE = "scikit.viewers"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def select_viewer(
|
|
20
|
+
viewers: Iterable[Viewer],
|
|
21
|
+
source: Path,
|
|
22
|
+
requested: Optional[str] = None,
|
|
23
|
+
) -> Viewer:
|
|
24
|
+
"""Pick the viewer that should handle ``source``.
|
|
25
|
+
|
|
26
|
+
An explicit ``requested`` name bypasses routing entirely: ``can_view`` is
|
|
27
|
+
not consulted and the viewer runs even if it would say no. Otherwise
|
|
28
|
+
viewers are ordered by ``(-priority, name)`` and the first whose
|
|
29
|
+
``can_view`` returns True wins.
|
|
30
|
+
|
|
31
|
+
A ``can_view`` that raises is logged and treated as ``False`` so a broken
|
|
32
|
+
viewer cannot take down routing for everyone else.
|
|
33
|
+
"""
|
|
34
|
+
viewers = list(viewers)
|
|
35
|
+
available = ", ".join(sorted(v.name for v in viewers))
|
|
36
|
+
|
|
37
|
+
if requested is not None:
|
|
38
|
+
for viewer in viewers:
|
|
39
|
+
if viewer.name == requested:
|
|
40
|
+
return viewer
|
|
41
|
+
raise NoViewerFound(
|
|
42
|
+
f"No viewer named {requested!r}. Available: {available or '(none)'}"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
ordered = sorted(viewers, key=lambda v: (-v.priority, v.name))
|
|
46
|
+
for viewer in ordered:
|
|
47
|
+
try:
|
|
48
|
+
if viewer.can_view(source):
|
|
49
|
+
return viewer
|
|
50
|
+
except Exception as exc: # noqa: BLE001 - a broken viewer must not break routing
|
|
51
|
+
logger.warning("viewer {!r}.can_view() raised: {}", viewer.name, exc)
|
|
52
|
+
|
|
53
|
+
raise NoViewerFound(
|
|
54
|
+
f"No viewer can handle {str(source)!r}. Available: {available or '(none)'}"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class Registry:
|
|
59
|
+
"""Loads viewers from entry points and remembers load failures."""
|
|
60
|
+
|
|
61
|
+
def __init__(self, namespace: str = NAMESPACE):
|
|
62
|
+
self.namespace = namespace
|
|
63
|
+
self._failed: list[tuple[str, Exception]] = []
|
|
64
|
+
self._manager = ExtensionManager(
|
|
65
|
+
namespace,
|
|
66
|
+
invoke_on_load=True,
|
|
67
|
+
on_load_failure_callback=self._on_load_failure,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def _on_load_failure(self, manager, entrypoint, exception):
|
|
71
|
+
self._failed.append((entrypoint.name, exception))
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def viewers(self) -> list[Viewer]:
|
|
75
|
+
return [ext.obj for ext in self._manager.extensions]
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def failed(self) -> list[tuple[str, Exception]]:
|
|
79
|
+
return list(self._failed)
|
|
80
|
+
|
|
81
|
+
def select(self, source: Path, requested: Optional[str] = None) -> Viewer:
|
|
82
|
+
try:
|
|
83
|
+
return select_viewer(self.viewers, source, requested)
|
|
84
|
+
except NoViewerFound as exc:
|
|
85
|
+
if self._failed:
|
|
86
|
+
detail = "; ".join(f"{name}: {err}" for name, err in self._failed)
|
|
87
|
+
raise NoViewerFound(f"{exc}\nLoad failures: {detail}") from exc
|
|
88
|
+
raise
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Core types for the scikit view framework.
|
|
2
|
+
|
|
3
|
+
This module defines the contract a viewer author implements, the result the
|
|
4
|
+
core builds, and the exception hierarchy callers branch on. It deliberately
|
|
5
|
+
contains no logic beyond that contract — routing, output-dir handling and the
|
|
6
|
+
CLI live elsewhere.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from abc import ABC, abstractmethod
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ScikitError(Exception):
|
|
15
|
+
"""Base class for all scikit errors. Catch this to handle them at once."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class NoViewerFound(ScikitError):
|
|
19
|
+
"""No viewer can handle the source, or the requested viewer name is unknown."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ViewFailed(ScikitError):
|
|
23
|
+
"""A viewer claimed the source but failed to produce a view."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class OutputDirNotEmpty(ScikitError):
|
|
27
|
+
"""A user-specified output directory is non-empty and force is not set."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Viewer(ABC):
|
|
31
|
+
"""Abstract base class a viewer implements.
|
|
32
|
+
|
|
33
|
+
A viewer turns one source file into an agent-readable markdown view,
|
|
34
|
+
writing any accompanying images into ``output_dir``.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
#: Unique viewer name, used for explicit selection (``--viewer``).
|
|
38
|
+
name: str
|
|
39
|
+
#: Routing priority; higher is consulted first. Ties broken by name.
|
|
40
|
+
priority: int = 0
|
|
41
|
+
|
|
42
|
+
@abstractmethod
|
|
43
|
+
def can_view(self, source: Path) -> bool:
|
|
44
|
+
"""Return True if this viewer can handle ``source``."""
|
|
45
|
+
|
|
46
|
+
@abstractmethod
|
|
47
|
+
def view(self, source: Path, output_dir: Path) -> str:
|
|
48
|
+
"""Produce the view.
|
|
49
|
+
|
|
50
|
+
Write images/attachments into ``output_dir`` and return the markdown
|
|
51
|
+
text. Attachments must be referenced with paths relative to
|
|
52
|
+
``output_dir`` so the directory can be moved as a whole.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True)
|
|
57
|
+
class ViewResult:
|
|
58
|
+
"""The outcome of a successful ``view()`` call, built entirely by the core."""
|
|
59
|
+
|
|
60
|
+
source: Path
|
|
61
|
+
output_dir: Path
|
|
62
|
+
viewer: str
|
|
63
|
+
markdown_path: Path
|
|
64
|
+
artifacts: tuple[Path, ...]
|