euporie 2.3.2__py3-none-any.whl → 2.4.1__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.
- euporie/console/__main__.py +3 -1
- euporie/console/app.py +6 -4
- euporie/console/tabs/console.py +34 -9
- euporie/core/__init__.py +6 -1
- euporie/core/__main__.py +1 -1
- euporie/core/app.py +79 -109
- euporie/core/border.py +44 -14
- euporie/core/comm/base.py +5 -4
- euporie/core/comm/ipywidgets.py +11 -11
- euporie/core/comm/registry.py +12 -6
- euporie/core/commands.py +30 -23
- euporie/core/completion.py +1 -4
- euporie/core/config.py +15 -5
- euporie/core/convert/{base.py → core.py} +117 -53
- euporie/core/convert/formats/ansi.py +46 -25
- euporie/core/convert/formats/base64.py +3 -3
- euporie/core/convert/formats/common.py +38 -13
- euporie/core/convert/formats/formatted_text.py +54 -12
- euporie/core/convert/formats/html.py +5 -5
- euporie/core/convert/formats/jpeg.py +1 -1
- euporie/core/convert/formats/markdown.py +4 -4
- euporie/core/convert/formats/pdf.py +1 -1
- euporie/core/convert/formats/pil.py +5 -3
- euporie/core/convert/formats/png.py +7 -6
- euporie/core/convert/formats/rich.py +4 -3
- euporie/core/convert/formats/sixel.py +5 -5
- euporie/core/convert/utils.py +1 -1
- euporie/core/current.py +11 -5
- euporie/core/formatted_text/ansi.py +4 -8
- euporie/core/formatted_text/html.py +1630 -856
- euporie/core/formatted_text/markdown.py +177 -166
- euporie/core/formatted_text/table.py +20 -14
- euporie/core/formatted_text/utils.py +21 -10
- euporie/core/io.py +14 -14
- euporie/core/kernel.py +48 -37
- euporie/core/key_binding/bindings/micro.py +5 -1
- euporie/core/key_binding/bindings/mouse.py +2 -2
- euporie/core/keys.py +3 -0
- euporie/core/launch.py +5 -2
- euporie/core/lexers.py +13 -2
- euporie/core/log.py +135 -139
- euporie/core/margins.py +32 -14
- euporie/core/path.py +273 -0
- euporie/core/processors.py +35 -0
- euporie/core/renderer.py +21 -5
- euporie/core/style.py +34 -19
- euporie/core/tabs/base.py +101 -17
- euporie/core/tabs/notebook.py +72 -30
- euporie/core/terminal.py +56 -48
- euporie/core/utils.py +12 -16
- euporie/core/widgets/cell.py +6 -5
- euporie/core/widgets/cell_outputs.py +2 -2
- euporie/core/widgets/decor.py +74 -82
- euporie/core/widgets/dialog.py +132 -28
- euporie/core/widgets/display.py +76 -24
- euporie/core/widgets/file_browser.py +87 -31
- euporie/core/widgets/formatted_text_area.py +1 -3
- euporie/core/widgets/forms.py +79 -40
- euporie/core/widgets/inputs.py +23 -13
- euporie/core/widgets/layout.py +4 -3
- euporie/core/widgets/menu.py +368 -216
- euporie/core/widgets/page.py +99 -58
- euporie/core/widgets/pager.py +1 -1
- euporie/core/widgets/palette.py +30 -27
- euporie/core/widgets/search_bar.py +38 -25
- euporie/core/widgets/status_bar.py +103 -5
- euporie/data/desktop/euporie-console.desktop +7 -0
- euporie/data/desktop/euporie-notebook.desktop +7 -0
- euporie/hub/__main__.py +3 -1
- euporie/hub/app.py +9 -7
- euporie/notebook/__main__.py +3 -1
- euporie/notebook/app.py +7 -30
- euporie/notebook/tabs/__init__.py +7 -3
- euporie/notebook/tabs/display.py +18 -9
- euporie/notebook/tabs/edit.py +106 -23
- euporie/notebook/tabs/json.py +73 -0
- euporie/notebook/tabs/log.py +18 -8
- euporie/notebook/tabs/notebook.py +60 -41
- euporie/preview/__main__.py +3 -1
- euporie/preview/app.py +2 -1
- euporie/preview/tabs/notebook.py +23 -10
- euporie/web/tabs/web.py +149 -0
- euporie/web/widgets/webview.py +563 -0
- euporie-2.4.1.data/data/share/applications/euporie-console.desktop +7 -0
- euporie-2.4.1.data/data/share/applications/euporie-notebook.desktop +7 -0
- {euporie-2.3.2.dist-info → euporie-2.4.1.dist-info}/METADATA +6 -5
- euporie-2.4.1.dist-info/RECORD +129 -0
- {euporie-2.3.2.dist-info → euporie-2.4.1.dist-info}/WHEEL +1 -1
- euporie/core/url.py +0 -64
- euporie-2.3.2.dist-info/RECORD +0 -122
- {euporie-2.3.2.dist-info → euporie-2.4.1.dist-info}/entry_points.txt +0 -0
- {euporie-2.3.2.dist-info → euporie-2.4.1.dist-info}/licenses/LICENSE +0 -0
euporie/core/comm/ipywidgets.py
CHANGED
@@ -41,7 +41,7 @@ from euporie.core.widgets.forms import (
|
|
41
41
|
from euporie.core.widgets.layout import AccordionSplit, ReferencedSplit, TabbedSplit
|
42
42
|
|
43
43
|
if TYPE_CHECKING:
|
44
|
-
from typing import Any, Iterable, Sequence
|
44
|
+
from typing import Any, Iterable, MutableSequence, Sequence
|
45
45
|
|
46
46
|
from prompt_toolkit.buffer import Buffer
|
47
47
|
from prompt_toolkit.formatted_text.base import AnyFormattedText
|
@@ -66,7 +66,7 @@ def _separate_buffers(
|
|
66
66
|
substate: dict | list | tuple,
|
67
67
|
path: list[str | int],
|
68
68
|
buffer_paths: list[list[str | int]],
|
69
|
-
buffers:
|
69
|
+
buffers: MutableSequence[memoryview | bytearray | bytes],
|
70
70
|
) -> dict | list | tuple:
|
71
71
|
"""Remove binary types from dicts and lists, but keep track of their paths.
|
72
72
|
|
@@ -118,7 +118,7 @@ def _separate_buffers(
|
|
118
118
|
cloned_substrate[k] = vnew
|
119
119
|
else:
|
120
120
|
raise ValueError("expected state to be a list or dict, not %r" % substate)
|
121
|
-
return cloned_substrate
|
121
|
+
return cloned_substrate if cloned_substrate is not None else substate
|
122
122
|
|
123
123
|
|
124
124
|
class IpyWidgetComm(Comm, metaclass=ABCMeta):
|
@@ -190,7 +190,7 @@ class IpyWidgetComm(Comm, metaclass=ABCMeta):
|
|
190
190
|
"model_module": state.get("_model_module"),
|
191
191
|
"model_module_version": state.get("_model_module_version"),
|
192
192
|
"state": {
|
193
|
-
**
|
193
|
+
**dict(state.items()),
|
194
194
|
"buffers": [
|
195
195
|
{
|
196
196
|
"encoding": "base64",
|
@@ -292,7 +292,7 @@ class OutputModel(IpyWidgetComm):
|
|
292
292
|
|
293
293
|
|
294
294
|
class LayoutIpyWidgetComm(IpyWidgetComm, metaclass=ABCMeta):
|
295
|
-
"""
|
295
|
+
"""Base class for layout widgets with children."""
|
296
296
|
|
297
297
|
def render_children(
|
298
298
|
self, models: list[str], parent: OutputParent
|
@@ -644,7 +644,7 @@ class FloatLogOptionsMixin(FloatOptionsMixin):
|
|
644
644
|
|
645
645
|
|
646
646
|
class SliderIpyWidgetComm(IpyWidgetComm, metaclass=ABCMeta):
|
647
|
-
"""
|
647
|
+
"""Base class for slider ipywidgets."""
|
648
648
|
|
649
649
|
def normalize(self, x: Any) -> Any:
|
650
650
|
"""Convert the internal widget's value to one compatible with the ipywidget."""
|
@@ -722,7 +722,7 @@ class SliderIpyWidgetComm(IpyWidgetComm, metaclass=ABCMeta):
|
|
722
722
|
|
723
723
|
|
724
724
|
class RangeSliderIpyWidgetComm(SliderIpyWidgetComm):
|
725
|
-
"""
|
725
|
+
"""Base class for range slider ipywidgets."""
|
726
726
|
|
727
727
|
@property
|
728
728
|
def indices(self) -> list[int]:
|
@@ -773,7 +773,7 @@ class FloatRangeSliderModel(FloatOptionsMixin, RangeSliderIpyWidgetComm):
|
|
773
773
|
|
774
774
|
|
775
775
|
class NumberTextBoxIpyWidgetComm(TextBoxIpyWidgetComm, metaclass=ABCMeta):
|
776
|
-
"""
|
776
|
+
"""Base class for text-box ipywidgets with numerical values."""
|
777
777
|
|
778
778
|
def create_view(self, parent: OutputParent) -> CommView:
|
779
779
|
"""Create a new view of the numerical text-box ipywidget."""
|
@@ -899,7 +899,7 @@ class FloatProgressModel(FloatOptionsMixin, ProgressIpyWidgetComm):
|
|
899
899
|
|
900
900
|
|
901
901
|
class ToggleableIpyWidgetComm(IpyWidgetComm, metaclass=ABCMeta):
|
902
|
-
"""
|
902
|
+
"""Base class for toggleable ipywidgets."""
|
903
903
|
|
904
904
|
def normalize(self, x: Any) -> float | None:
|
905
905
|
"""Cat the container's selected value to a bool if possible."""
|
@@ -994,7 +994,7 @@ class ValidModel(ToggleableIpyWidgetComm):
|
|
994
994
|
|
995
995
|
|
996
996
|
class SelectableIpyWidgetComm(IpyWidgetComm, metaclass=ABCMeta):
|
997
|
-
"""
|
997
|
+
"""Base class for selectable ipywidgets."""
|
998
998
|
|
999
999
|
def update_index(self, container: SelectableWidget) -> None:
|
1000
1000
|
"""Send a ``comm_message`` updating the selected index when it changes."""
|
@@ -1098,7 +1098,7 @@ class SelectMultipleModel(IpyWidgetComm):
|
|
1098
1098
|
options=self.data["state"].get("_options_labels", []),
|
1099
1099
|
indices=self.data["state"]["index"],
|
1100
1100
|
on_change=self.update_index,
|
1101
|
-
style="class:select
|
1101
|
+
style="class:select",
|
1102
1102
|
multiple=True,
|
1103
1103
|
rows=self.data["state"]["rows"],
|
1104
1104
|
disabled=Condition(lambda: self.data["state"].get("disabled", False)),
|
euporie/core/comm/registry.py
CHANGED
@@ -8,11 +8,13 @@ from euporie.core.comm.base import UnimplementedComm
|
|
8
8
|
from euporie.core.comm.ipywidgets import open_comm_ipywidgets
|
9
9
|
|
10
10
|
if TYPE_CHECKING:
|
11
|
-
from typing import Any, Sequence
|
11
|
+
from typing import Any, Callable, Sequence
|
12
12
|
|
13
13
|
from euporie.core.comm.base import Comm, KernelTab
|
14
14
|
|
15
|
-
TARGET_CLASSES = {
|
15
|
+
TARGET_CLASSES: dict[str, Callable[[KernelTab, str, dict, Sequence[bytes]], Comm]] = {
|
16
|
+
"jupyter.widget": open_comm_ipywidgets
|
17
|
+
}
|
16
18
|
|
17
19
|
|
18
20
|
def open_comm(
|
@@ -36,8 +38,12 @@ def open_comm(
|
|
36
38
|
"""
|
37
39
|
target_name = content.get("target_name", "")
|
38
40
|
return TARGET_CLASSES.get(target_name, UnimplementedComm)(
|
39
|
-
comm_container=
|
40
|
-
|
41
|
-
|
42
|
-
|
41
|
+
# comm_container=
|
42
|
+
comm_container,
|
43
|
+
# comm_id=
|
44
|
+
str(content.get("comm_id")),
|
45
|
+
# data=
|
46
|
+
content.get("data", {}),
|
47
|
+
# buffers=
|
48
|
+
buffers,
|
43
49
|
)
|
euporie/core/commands.py
CHANGED
@@ -16,18 +16,26 @@ from euporie.core.key_binding.utils import parse_keys
|
|
16
16
|
from euporie.core.keys import Keys
|
17
17
|
|
18
18
|
if TYPE_CHECKING:
|
19
|
-
from typing import Any, Callable, Coroutine
|
19
|
+
from typing import Any, Callable, Coroutine
|
20
20
|
|
21
21
|
from prompt_toolkit.filters import Filter, FilterOrBool
|
22
22
|
from prompt_toolkit.key_binding.key_bindings import (
|
23
23
|
KeyBindingsBase,
|
24
24
|
KeyHandlerCallable,
|
25
|
+
NotImplementedOrNone,
|
25
26
|
)
|
26
27
|
|
27
28
|
from euporie.core.widgets.menu import MenuItem
|
28
29
|
|
29
30
|
AnyKey = tuple[Keys | str, ...] | Keys | str
|
30
31
|
AnyKeys = list[AnyKey] | AnyKey
|
32
|
+
CommandHandlerNoArgs = Callable[
|
33
|
+
..., Coroutine[Any, Any, None] | NotImplementedOrNone
|
34
|
+
]
|
35
|
+
CommandHandlerArgs = Callable[
|
36
|
+
[KeyPressEvent], Coroutine[Any, Any, None] | NotImplementedOrNone
|
37
|
+
]
|
38
|
+
CommandHandler = CommandHandlerNoArgs | CommandHandlerArgs
|
31
39
|
|
32
40
|
log = logging.getLogger(__name__)
|
33
41
|
|
@@ -37,7 +45,7 @@ class Command:
|
|
37
45
|
|
38
46
|
def __init__(
|
39
47
|
self,
|
40
|
-
handler:
|
48
|
+
handler: CommandHandler,
|
41
49
|
*,
|
42
50
|
filter: FilterOrBool = True,
|
43
51
|
hidden: FilterOrBool = False,
|
@@ -98,9 +106,6 @@ class Command:
|
|
98
106
|
|
99
107
|
self.keys: list[tuple[str | Keys, ...]] = []
|
100
108
|
|
101
|
-
self.selected_item = 0
|
102
|
-
self.children: Sequence[MenuItem] = []
|
103
|
-
|
104
109
|
def run(self) -> None:
|
105
110
|
"""Run the command's handler."""
|
106
111
|
if self.filter():
|
@@ -117,28 +122,28 @@ class Command:
|
|
117
122
|
@property
|
118
123
|
def key_handler(self) -> KeyHandlerCallable:
|
119
124
|
"""Return a key handler for the command."""
|
120
|
-
|
125
|
+
handler = self.handler
|
126
|
+
sig = signature(handler)
|
121
127
|
|
122
128
|
if sig.parameters:
|
123
129
|
# The handler already accepts a `KeyPressEvent` argument
|
124
|
-
return cast("KeyHandlerCallable",
|
130
|
+
return cast("KeyHandlerCallable", handler)
|
131
|
+
|
132
|
+
if isawaitable(handler):
|
125
133
|
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
awaitable = result
|
134
|
+
async def _key_handler_async(event: KeyPressEvent) -> NotImplementedOrNone:
|
135
|
+
result = cast("CommandHandlerNoArgs", handler)()
|
136
|
+
assert isawaitable(result)
|
137
|
+
return await result
|
131
138
|
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
event.app.invalidate()
|
139
|
+
return _key_handler_async
|
140
|
+
|
141
|
+
else:
|
136
142
|
|
137
|
-
|
138
|
-
|
139
|
-
event.app.invalidate()
|
143
|
+
def _key_handler(event: KeyPressEvent) -> NotImplementedOrNone:
|
144
|
+
return cast("CommandHandlerNoArgs", handler)()
|
140
145
|
|
141
|
-
|
146
|
+
return _key_handler
|
142
147
|
|
143
148
|
def bind(self, key_bindings: KeyBindingsBase, keys: AnyKeys) -> None:
|
144
149
|
"""Add the current commands to a set of key bindings.
|
@@ -173,16 +178,18 @@ class Command:
|
|
173
178
|
@property
|
174
179
|
def menu_handler(self) -> Callable[[], None]:
|
175
180
|
"""Return a menu handler for the command."""
|
176
|
-
|
181
|
+
handler = self.handler
|
182
|
+
if isawaitable(handler):
|
177
183
|
|
178
184
|
def _menu_handler() -> None:
|
179
|
-
task =
|
185
|
+
task = cast("CommandHandlerNoArgs", handler)()
|
186
|
+
task = cast("Coroutine[Any, Any, None]", task)
|
180
187
|
if task is not None:
|
181
188
|
get_app().create_background_task(task)
|
182
189
|
|
183
190
|
return _menu_handler
|
184
191
|
else:
|
185
|
-
return cast("Callable[[], None]",
|
192
|
+
return cast("Callable[[], None]", handler)
|
186
193
|
|
187
194
|
@property
|
188
195
|
def menu(self) -> MenuItem:
|
euporie/core/completion.py
CHANGED
@@ -41,8 +41,5 @@ class KernelCompleter(Completer):
|
|
41
41
|
cursor_pos=document.cursor_position,
|
42
42
|
):
|
43
43
|
if completion_type := kwargs.get("display_meta"):
|
44
|
-
kwargs["style"] = f"class:completion-
|
45
|
-
kwargs[
|
46
|
-
"selected_style"
|
47
|
-
] = f"class:completion-menu.completion.current.{completion_type}"
|
44
|
+
kwargs["style"] = f"class:completion-{completion_type}"
|
48
45
|
yield Completion(**kwargs)
|
euporie/core/config.py
CHANGED
@@ -6,6 +6,7 @@ import argparse
|
|
6
6
|
import json
|
7
7
|
import logging
|
8
8
|
import os
|
9
|
+
import sys
|
9
10
|
from ast import literal_eval
|
10
11
|
from collections import ChainMap
|
11
12
|
from functools import partial
|
@@ -13,7 +14,7 @@ from pathlib import Path
|
|
13
14
|
from typing import TYPE_CHECKING, Protocol, TextIO, cast
|
14
15
|
|
15
16
|
import fastjsonschema
|
16
|
-
from
|
17
|
+
from platformdirs import user_config_dir
|
17
18
|
from prompt_toolkit.filters.base import Condition
|
18
19
|
from prompt_toolkit.filters.utils import to_filter
|
19
20
|
from prompt_toolkit.utils import Event
|
@@ -21,7 +22,6 @@ from upath import UPath
|
|
21
22
|
|
22
23
|
from euporie.core import __app_name__, __copyright__, __version__
|
23
24
|
from euporie.core.commands import add_cmd, get_cmd
|
24
|
-
from euporie.core.log import setup_logs
|
25
25
|
|
26
26
|
if TYPE_CHECKING:
|
27
27
|
from typing import IO, Any, Callable, Optional, Sequence
|
@@ -339,6 +339,8 @@ class Config:
|
|
339
339
|
|
340
340
|
def load(self, cls: type[ConfigurableApp]) -> None:
|
341
341
|
"""Load the command line, environment, and user configuration."""
|
342
|
+
from euporie.core.log import setup_logs
|
343
|
+
|
342
344
|
self.app_cls = cls
|
343
345
|
self.app_name = cls.name
|
344
346
|
log.debug("Loading config for %s", self.app_name)
|
@@ -416,7 +418,11 @@ class Config:
|
|
416
418
|
def load_args(self) -> dict[str, Any]:
|
417
419
|
"""Attempt to load configuration settings from commandline flags."""
|
418
420
|
result = {}
|
419
|
-
|
421
|
+
# Parse known arguments
|
422
|
+
namespace, remainder = self.load_parser().parse_known_intermixed_args()
|
423
|
+
# Update argv to leave the remaining arguments for subsequent apps
|
424
|
+
sys.argv[1:] = remainder
|
425
|
+
# Validate arguments
|
420
426
|
for name, value in vars(namespace).items():
|
421
427
|
if value is not None:
|
422
428
|
# Convert to json and back to attain json types
|
@@ -508,17 +514,21 @@ class Config:
|
|
508
514
|
results.update(json_data)
|
509
515
|
return results
|
510
516
|
|
511
|
-
def get(self, name: str) -> Any:
|
517
|
+
def get(self, name: str, default: Any = None) -> Any:
|
512
518
|
"""Access a configuration value, falling back to the default value if unset.
|
513
519
|
|
514
520
|
Args:
|
515
521
|
name: The name of the attribute to access.
|
522
|
+
default: The value to return if the name is not found
|
516
523
|
|
517
524
|
Returns:
|
518
525
|
The configuration variable value.
|
519
526
|
|
520
527
|
"""
|
521
|
-
|
528
|
+
if name in self.settings:
|
529
|
+
return self.settings[name].value
|
530
|
+
else:
|
531
|
+
return default
|
522
532
|
|
523
533
|
def get_item(self, name: str) -> Any:
|
524
534
|
"""Access a configuration item.
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
4
4
|
|
5
5
|
import logging
|
6
6
|
import mimetypes
|
7
|
-
from functools import partial
|
7
|
+
from functools import lru_cache, partial
|
8
8
|
from typing import TYPE_CHECKING, NamedTuple
|
9
9
|
|
10
10
|
from prompt_toolkit.cache import FastDictCache, SimpleCache
|
@@ -12,7 +12,10 @@ from prompt_toolkit.filters import to_filter
|
|
12
12
|
from upath import UPath
|
13
13
|
from upath.implementations.http import HTTPPath
|
14
14
|
|
15
|
+
from euporie.core.path import DataPath
|
16
|
+
|
15
17
|
if TYPE_CHECKING:
|
18
|
+
from pathlib import Path
|
16
19
|
from typing import Any, Callable, Iterable
|
17
20
|
|
18
21
|
from prompt_toolkit.filters import Filter, FilterOrBool
|
@@ -44,19 +47,71 @@ ERROR_OUTPUTS = {
|
|
44
47
|
"formatted_text": [("fg:white bg:darkred", "(Format Conversion Error)")],
|
45
48
|
}
|
46
49
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
50
|
+
|
51
|
+
@lru_cache
|
52
|
+
def get_mime(path: Path | str) -> str | None:
|
53
|
+
"""Attempt to determine the mime-type of a path."""
|
54
|
+
if isinstance(path, str):
|
55
|
+
path = UPath(path)
|
56
|
+
try:
|
57
|
+
path = path.resolve()
|
58
|
+
except Exception:
|
59
|
+
log.debug("Cannot resolve '%s'", path)
|
60
|
+
|
61
|
+
mime = None
|
62
|
+
|
63
|
+
# Read from path of data URI
|
64
|
+
if isinstance(path, DataPath):
|
65
|
+
mime = path._mime
|
66
|
+
|
67
|
+
# Guess from file-extension
|
68
|
+
if not mime and path.suffix:
|
69
|
+
# Check for Jupyter notebooks by extension
|
70
|
+
if path.suffix == ".ipynb":
|
71
|
+
return "application/x-ipynb+json"
|
72
|
+
else:
|
73
|
+
mime, _ = mimetypes.guess_type(path)
|
74
|
+
|
75
|
+
# Try using magic
|
76
|
+
if not mime:
|
77
|
+
try:
|
78
|
+
import magic
|
79
|
+
except ModuleNotFoundError:
|
80
|
+
pass
|
81
|
+
else:
|
82
|
+
with path.open(mode="rb") as f:
|
83
|
+
mime = magic.from_buffer(f.read(2048), mime=True)
|
84
|
+
|
85
|
+
# If we have a web-address, nsure we have a url
|
86
|
+
# Check http-headers and nsure we have a url
|
87
|
+
if not mime and isinstance(path, HTTPPath) and path._url is not None:
|
88
|
+
from fsspec.asyn import sync
|
89
|
+
|
90
|
+
# Get parsed url
|
91
|
+
url = path._url.geturl()
|
92
|
+
# Get the fsspec fs
|
93
|
+
fs = path._accessor._fs
|
94
|
+
# Ensure we have a session
|
95
|
+
session = sync(fs.loop, fs.set_session)
|
96
|
+
# Use HEAD requests if the server allows it, falling back to GETs
|
97
|
+
for method in (session.head, session.get):
|
98
|
+
r = sync(fs.loop, method, url, allow_redirects=True)
|
99
|
+
try:
|
100
|
+
r.raise_for_status()
|
101
|
+
except Exception:
|
102
|
+
log.debug("Request failed: %s", r)
|
103
|
+
continue
|
104
|
+
else:
|
105
|
+
content_type = r.headers.get("Content-Type")
|
106
|
+
if content_type is not None:
|
107
|
+
mime = content_type.partition(";")[0]
|
108
|
+
break
|
109
|
+
|
110
|
+
return mime
|
57
111
|
|
58
112
|
|
59
|
-
|
113
|
+
@lru_cache
|
114
|
+
def get_format(path: Path | str, default: str = "") -> str:
|
60
115
|
"""Attempt to guess the format of a path."""
|
61
116
|
if isinstance(path, str):
|
62
117
|
path = UPath(path)
|
@@ -65,7 +120,7 @@ def get_format(path: UPath | str, default: str = "") -> str:
|
|
65
120
|
default = "html"
|
66
121
|
else:
|
67
122
|
default = "ansi"
|
68
|
-
mime
|
123
|
+
mime = get_mime(path)
|
69
124
|
return MIME_FORMATS.get(mime, default) if mime else default
|
70
125
|
|
71
126
|
|
@@ -144,7 +199,7 @@ def find_route(from_: str, to: str) -> list | None:
|
|
144
199
|
for step_a, step_b in zip(chain, chain[1:])
|
145
200
|
]
|
146
201
|
),
|
147
|
-
)
|
202
|
+
)
|
148
203
|
else:
|
149
204
|
return None
|
150
205
|
|
@@ -162,7 +217,7 @@ def convert(
|
|
162
217
|
rows: int | None = None,
|
163
218
|
fg: str | None = None,
|
164
219
|
bg: str | None = None,
|
165
|
-
path:
|
220
|
+
path: Path | None = None,
|
166
221
|
) -> Any:
|
167
222
|
"""Convert between formats."""
|
168
223
|
try:
|
@@ -179,49 +234,55 @@ def convert(
|
|
179
234
|
rows: int | None = None,
|
180
235
|
fg: str | None = None,
|
181
236
|
bg: str | None = None,
|
182
|
-
path:
|
237
|
+
path: Path | None = None,
|
183
238
|
) -> Any:
|
184
239
|
if from_ == to:
|
185
240
|
return data
|
186
|
-
|
187
|
-
|
188
|
-
if
|
241
|
+
routes = _CONVERTOR_ROUTE_CACHE[(from_, to)]
|
242
|
+
log.debug("Converting from '%s' to '%s' using route: %s", from_, to, routes)
|
243
|
+
if not routes:
|
189
244
|
raise NotImplementedError(f"Cannot convert from `{from_}` to `{to}`")
|
190
245
|
output: Any = data
|
191
|
-
for
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
246
|
+
for route in routes:
|
247
|
+
for stage_a, stage_b in zip(route, route[1:]):
|
248
|
+
# Find converter with lowest weight
|
249
|
+
func = sorted(
|
250
|
+
[
|
251
|
+
conv
|
252
|
+
for conv in converters[stage_b][stage_a]
|
253
|
+
if _FILTER_CACHE.get((conv,), conv.filter_)
|
254
|
+
],
|
255
|
+
key=lambda x: x.weight,
|
256
|
+
)[0].func
|
257
|
+
# Add intermediate steps to the cache
|
258
|
+
try:
|
259
|
+
output_hash = hash(data)
|
260
|
+
except TypeError as error:
|
261
|
+
log.warning("Cannot hash %s", data)
|
262
|
+
raise error
|
263
|
+
try:
|
264
|
+
output = _CONVERSION_CACHE.get(
|
265
|
+
(output_hash, from_, stage_b, cols, rows, fg, bg, path),
|
266
|
+
partial(func, output, cols, rows, fg, bg, path),
|
267
|
+
)
|
268
|
+
except Exception:
|
269
|
+
log.exception("An error occurred during format conversion")
|
270
|
+
output = None
|
271
|
+
if output is None:
|
272
|
+
log.error(
|
273
|
+
"Failed to convert `%s` from `%s`"
|
274
|
+
" to `%s` using route `%s` at stage `%s`",
|
275
|
+
data.__repr__()[:10],
|
276
|
+
from_,
|
277
|
+
to,
|
278
|
+
route,
|
279
|
+
stage_b,
|
280
|
+
)
|
281
|
+
# Try the next route on error
|
282
|
+
break
|
283
|
+
else:
|
284
|
+
# If this route succeeded, stop trying routes
|
285
|
+
break
|
225
286
|
return output
|
226
287
|
|
227
288
|
data = _CONVERSION_CACHE.get(
|
@@ -229,4 +290,7 @@ def convert(
|
|
229
290
|
partial(_convert, data, from_, to, cols, rows, fg, bg, path),
|
230
291
|
)
|
231
292
|
|
293
|
+
if data is None:
|
294
|
+
data = ERROR_OUTPUTS.get(to, b"(Conversion Error)")
|
295
|
+
|
232
296
|
return data
|