cuiman 0.3.0__tar.gz → 0.4.0__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.
- {cuiman-0.3.0 → cuiman-0.4.0}/.gitignore +1 -1
- {cuiman-0.3.0 → cuiman-0.4.0}/PKG-INFO +5 -3
- {cuiman-0.3.0 → cuiman-0.4.0}/cuiman/api/__init__.py +13 -0
- cuiman-0.4.0/cuiman/api/assets/__init__.py +76 -0
- cuiman-0.4.0/cuiman/api/assets/common.py +208 -0
- cuiman-0.4.0/cuiman/api/assets/job_results.py +71 -0
- cuiman-0.4.0/cuiman/api/assets/stac.py +454 -0
- {cuiman-0.3.0 → cuiman-0.4.0}/cuiman/api/async_client.py +1 -1
- {cuiman-0.3.0 → cuiman-0.4.0}/cuiman/api/async_client_mixin.py +118 -21
- cuiman-0.4.0/cuiman/api/branding.py +84 -0
- {cuiman-0.3.0 → cuiman-0.4.0}/cuiman/api/client.py +1 -1
- {cuiman-0.3.0 → cuiman-0.4.0}/cuiman/api/client_mixin.py +117 -25
- {cuiman-0.3.0 → cuiman-0.4.0}/cuiman/api/client_mixin_base.py +71 -3
- {cuiman-0.3.0 → cuiman-0.4.0}/cuiman/api/config.py +99 -12
- {cuiman-0.3.0 → cuiman-0.4.0}/cuiman/api/exceptions.py +1 -1
- {cuiman-0.3.0 → cuiman-0.4.0}/cuiman/api/jobs.py +10 -3
- {cuiman-0.3.0 → cuiman-0.4.0}/cuiman/api/opener/__init__.py +2 -1
- cuiman-0.4.0/cuiman/api/opener/context.py +245 -0
- {cuiman-0.3.0 → cuiman-0.4.0}/cuiman/api/opener/errors.py +14 -1
- {cuiman-0.3.0 → cuiman-0.4.0}/cuiman/api/opener/impl/__init__.py +4 -0
- {cuiman-0.3.0 → cuiman-0.4.0}/cuiman/api/opener/impl/_gpd.py +2 -0
- {cuiman-0.3.0 → cuiman-0.4.0}/cuiman/api/opener/impl/_image.py +14 -1
- cuiman-0.4.0/cuiman/api/opener/impl/_paths.py +27 -0
- {cuiman-0.3.0 → cuiman-0.4.0}/cuiman/api/opener/impl/_pd.py +2 -0
- cuiman-0.4.0/cuiman/api/opener/impl/_stac/__init__.py +1 -0
- cuiman-0.4.0/cuiman/api/opener/impl/_stac/asset.py +104 -0
- cuiman-0.4.0/cuiman/api/opener/impl/_stac/composition.py +111 -0
- cuiman-0.4.0/cuiman/api/opener/impl/_stac/locations.py +59 -0
- cuiman-0.4.0/cuiman/api/opener/impl/_stac/opener.py +145 -0
- cuiman-0.4.0/cuiman/api/opener/impl/_stac/reader.py +613 -0
- cuiman-0.4.0/cuiman/api/opener/impl/_stac/support.py +82 -0
- {cuiman-0.3.0 → cuiman-0.4.0}/cuiman/api/opener/impl/_xr.py +2 -0
- {cuiman-0.3.0 → cuiman-0.4.0}/cuiman/api/opener/impl/base.py +42 -33
- {cuiman-0.3.0 → cuiman-0.4.0}/cuiman/api/opener/impl/openers.py +12 -1
- {cuiman-0.3.0 → cuiman-0.4.0}/cuiman/api/opener/opener.py +55 -10
- {cuiman-0.3.0 → cuiman-0.4.0}/cuiman/api/opener/registry.py +2 -0
- {cuiman-0.3.0 → cuiman-0.4.0}/cuiman/api/service_client.py +2 -2
- {cuiman-0.3.0 → cuiman-0.4.0}/cuiman/api/transport/args.py +1 -1
- {cuiman-0.3.0 → cuiman-0.4.0}/cuiman/api/transport/httpx2.py +17 -2
- {cuiman-0.3.0 → cuiman-0.4.0}/cuiman/api/transport/transport.py +12 -0
- {cuiman-0.3.0 → cuiman-0.4.0}/cuiman/app/__init__.py +5 -0
- {cuiman-0.3.0 → cuiman-0.4.0}/cuiman/app/app.py +1 -1
- cuiman-0.4.0/cuiman/app/dist/assets/github-CDab0zVI.css +1 -0
- cuiman-0.4.0/cuiman/app/dist/assets/github-dark-Dfs9RUU9.css +1 -0
- cuiman-0.4.0/cuiman/app/dist/assets/index-7PXY_U-I.js +90 -0
- cuiman-0.4.0/cuiman/app/dist/assets/index-BCVM7_WB.css +1 -0
- {cuiman-0.3.0 → cuiman-0.4.0}/cuiman/app/dist/index.html +2 -2
- {cuiman-0.3.0 → cuiman-0.4.0}/cuiman/app/launch.py +91 -0
- {cuiman-0.3.0 → cuiman-0.4.0}/cuiman/app/serve.py +5 -1
- {cuiman-0.3.0 → cuiman-0.4.0}/cuiman/cli/config.py +8 -2
- {cuiman-0.3.0 → cuiman-0.4.0}/cuiman/cli/output.py +1 -1
- {cuiman-0.3.0 → cuiman-0.4.0}/pyproject.toml +5 -2
- cuiman-0.3.0/cuiman/api/opener/context.py +0 -165
- cuiman-0.3.0/cuiman/app/dist/assets/index-Br7nJ6aR.js +0 -85
- cuiman-0.3.0/cuiman/app/dist/assets/index-DKE7nU5k.css +0 -1
- {cuiman-0.3.0 → cuiman-0.4.0}/LICENSE +0 -0
- {cuiman-0.3.0 → cuiman-0.4.0}/README.md +0 -0
- {cuiman-0.3.0 → cuiman-0.4.0}/cuiman/__init__.py +0 -0
- {cuiman-0.3.0 → cuiman-0.4.0}/cuiman/api/auth/__init__.py +0 -0
- {cuiman-0.3.0 → cuiman-0.4.0}/cuiman/api/auth/config.py +0 -0
- {cuiman-0.3.0 → cuiman-0.4.0}/cuiman/api/auth/interactive.py +0 -0
- {cuiman-0.3.0 → cuiman-0.4.0}/cuiman/api/auth/login.py +0 -0
- {cuiman-0.3.0 → cuiman-0.4.0}/cuiman/api/auth/oauth2_client.py +0 -0
- {cuiman-0.3.0 → cuiman-0.4.0}/cuiman/api/auth/oidc.py +0 -0
- {cuiman-0.3.0 → cuiman-0.4.0}/cuiman/api/auth/secret_store.py +0 -0
- {cuiman-0.3.0 → cuiman-0.4.0}/cuiman/api/client_app_mixin.py +0 -0
- {cuiman-0.3.0 → cuiman-0.4.0}/cuiman/api/defaults.py +0 -0
- {cuiman-0.3.0 → cuiman-0.4.0}/cuiman/api/ishell.py +0 -0
- {cuiman-0.3.0 → cuiman-0.4.0}/cuiman/api/transport/__init__.py +0 -0
- {cuiman-0.3.0 → cuiman-0.4.0}/cuiman/app/display.py +0 -0
- {cuiman-0.3.0 → cuiman-0.4.0}/cuiman/app/dist/eozilla-small.svg +0 -0
- {cuiman-0.3.0 → cuiman-0.4.0}/cuiman/app/dist/eozilla.svg +0 -0
- {cuiman-0.3.0 → cuiman-0.4.0}/cuiman/app/dist/privacy.md +0 -0
- {cuiman-0.3.0 → cuiman-0.4.0}/cuiman/app/notebook-display.js +0 -0
- {cuiman-0.3.0 → cuiman-0.4.0}/cuiman/app/url.py +0 -0
- {cuiman-0.3.0 → cuiman-0.4.0}/cuiman/cli/__init__.py +0 -0
- {cuiman-0.3.0 → cuiman-0.4.0}/cuiman/cli/cli.py +0 -0
- {cuiman-0.3.0 → cuiman-0.4.0}/cuiman/cli/client.py +0 -0
- {cuiman-0.3.0 → cuiman-0.4.0}/cuiman/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: cuiman
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Provides a client Python API, GUI, and CLI for servers compliant with OGC API - Processes
|
|
5
5
|
Project-URL: Documentation, https://eo-tools.github.io/eozilla
|
|
6
6
|
Project-URL: Repository, https://github.com/eo-tools/eozilla
|
|
@@ -28,7 +28,7 @@ Classifier: Topic :: Software Development
|
|
|
28
28
|
Classifier: Typing :: Typed
|
|
29
29
|
Requires-Python: >=3.10
|
|
30
30
|
Requires-Dist: authlib<1.9,>=1.8
|
|
31
|
-
Requires-Dist: gavicore>=0.
|
|
31
|
+
Requires-Dist: gavicore>=0.4.0
|
|
32
32
|
Requires-Dist: httpx2
|
|
33
33
|
Requires-Dist: ipython
|
|
34
34
|
Requires-Dist: joserfc<2,>=1.6
|
|
@@ -39,6 +39,8 @@ Requires-Dist: pyyaml
|
|
|
39
39
|
Requires-Dist: remotestate<1,>=0.3.4
|
|
40
40
|
Requires-Dist: typer<1,>=0.26.0
|
|
41
41
|
Requires-Dist: uri-template
|
|
42
|
+
Provides-Extra: stac
|
|
43
|
+
Requires-Dist: pystac<1.16,>=1.15.2; extra == 'stac'
|
|
42
44
|
Description-Content-Type: text/markdown
|
|
43
45
|
|
|
44
46
|
# Eozilla Cuiman
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
# Permissions are hereby granted under the terms of the Apache 2.0 License:
|
|
3
3
|
# https://opensource.org/license/apache-2-0.
|
|
4
4
|
|
|
5
|
+
from gavicore.models.core import JobResults
|
|
6
|
+
|
|
5
7
|
from .async_client import AsyncClient
|
|
6
8
|
from .client import Client
|
|
7
9
|
from .config import ClientConfig
|
|
@@ -16,3 +18,14 @@ __all__ = [
|
|
|
16
18
|
"JobMonitor",
|
|
17
19
|
"JobOptions",
|
|
18
20
|
]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _repr_job_results_as_html(self: JobResults) -> str:
|
|
24
|
+
"""Render job outputs in notebooks without fetching or displaying data."""
|
|
25
|
+
from .assets.job_results import _job_results_html
|
|
26
|
+
|
|
27
|
+
return _job_results_html(self)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# Install Cuiman's presentation without introducing a gavicore dependency on it.
|
|
31
|
+
JobResults._repr_html_ = _repr_job_results_as_html # type: ignore[attr-defined]
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Copyright (c) 2026 by the Eozilla team and contributors
|
|
2
|
+
# Permissions are hereby granted under the terms of the Apache 2.0 License:
|
|
3
|
+
# https://opensource.org/license/apache-2-0.
|
|
4
|
+
|
|
5
|
+
"""Notebook display utilities for STAC assets and job results."""
|
|
6
|
+
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Callable, Iterable, Sequence
|
|
8
|
+
|
|
9
|
+
from gavicore.models.core import JobResults, QualifiedValue
|
|
10
|
+
|
|
11
|
+
from .common import _get_items_expression
|
|
12
|
+
from .stac import as_stac_asset
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from pystac import Asset, Item, ItemCollection
|
|
16
|
+
|
|
17
|
+
__all__ = ["as_stac_asset", "display_assets"]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
async def display_assets(
|
|
21
|
+
items: "JobResults | QualifiedValue | dict[str, Any] | Item | Sequence[Item] | ItemCollection",
|
|
22
|
+
*,
|
|
23
|
+
roles: str | Iterable[str] | None = None,
|
|
24
|
+
previews: bool = True,
|
|
25
|
+
open_asset: "Callable[[Asset], Any] | None" = None,
|
|
26
|
+
) -> None:
|
|
27
|
+
"""Render job result tables or grouped STAC assets and optional previews.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
items: JobResults, STAC objects, or inline STAC JSON dictionaries
|
|
31
|
+
(optionally wrapped in QualifiedValue).
|
|
32
|
+
roles: Show assets matching any of these roles. A string matches one
|
|
33
|
+
role, None shows all assets, and an empty iterable matches none.
|
|
34
|
+
Job result rows are not filtered by roles.
|
|
35
|
+
previews: Open and display thumbnail, overview, and visual assets.
|
|
36
|
+
Set to ``False`` to prevent asset preview reads. Geometry previews
|
|
37
|
+
remain enabled. Asset previews are independent of the table's
|
|
38
|
+
role filter; unreadable or unsupported previews are skipped.
|
|
39
|
+
open_asset: A synchronous or asynchronous asset opener. If omitted,
|
|
40
|
+
asset previews are skipped; tables and geometry maps remain enabled.
|
|
41
|
+
Job result tables never open linked outputs.
|
|
42
|
+
"""
|
|
43
|
+
await _display_assets(
|
|
44
|
+
items,
|
|
45
|
+
expression=_get_items_expression("display_assets"),
|
|
46
|
+
roles=roles,
|
|
47
|
+
previews=previews,
|
|
48
|
+
open_asset=open_asset,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
async def _display_assets(
|
|
53
|
+
items: "JobResults | QualifiedValue | dict[str, Any] | Item | Sequence[Item] | ItemCollection",
|
|
54
|
+
*,
|
|
55
|
+
expression: str | None,
|
|
56
|
+
roles: str | Iterable[str] | None = None,
|
|
57
|
+
previews: bool = True,
|
|
58
|
+
open_asset: "Callable[[Asset], Any] | None" = None,
|
|
59
|
+
) -> None:
|
|
60
|
+
"""Dispatch notebook display without importing optional STAC dependencies."""
|
|
61
|
+
if isinstance(items, JobResults):
|
|
62
|
+
from IPython.display import display
|
|
63
|
+
|
|
64
|
+
from .job_results import _render_job_results
|
|
65
|
+
|
|
66
|
+
display(_render_job_results(items, expression=expression))
|
|
67
|
+
else:
|
|
68
|
+
from .stac import _display_stac_assets
|
|
69
|
+
|
|
70
|
+
await _display_stac_assets(
|
|
71
|
+
items,
|
|
72
|
+
expression=expression,
|
|
73
|
+
roles=roles,
|
|
74
|
+
previews=previews,
|
|
75
|
+
open_asset=open_asset,
|
|
76
|
+
)
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# Copyright (c) 2026 by the Eozilla team and contributors
|
|
2
|
+
# Permissions are hereby granted under the terms of the Apache 2.0 License:
|
|
3
|
+
# https://opensource.org/license/apache-2-0.
|
|
4
|
+
|
|
5
|
+
"""Shared notebook styles, links, copy buttons, and caller expressions."""
|
|
6
|
+
|
|
7
|
+
from html import escape
|
|
8
|
+
from inspect import currentframe
|
|
9
|
+
from types import FrameType
|
|
10
|
+
from urllib.parse import unquote, urlsplit
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _stac_label(value: object) -> str | None:
|
|
14
|
+
"""Recognize inline STAC items and collections without optional imports."""
|
|
15
|
+
if not isinstance(value, dict):
|
|
16
|
+
return None
|
|
17
|
+
if (
|
|
18
|
+
value.get("type") == "Feature"
|
|
19
|
+
and isinstance(value.get("stac_version"), str)
|
|
20
|
+
and isinstance(value.get("assets"), dict)
|
|
21
|
+
):
|
|
22
|
+
return "STAC Item"
|
|
23
|
+
features = value.get("features")
|
|
24
|
+
if (
|
|
25
|
+
value.get("type") == "FeatureCollection"
|
|
26
|
+
and isinstance(features, list)
|
|
27
|
+
and features
|
|
28
|
+
and all(_stac_label(feature) == "STAC Item" for feature in features)
|
|
29
|
+
):
|
|
30
|
+
return "STAC ItemCollection"
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _styles() -> str:
|
|
35
|
+
"""Return the shared asset table and card styles."""
|
|
36
|
+
return (
|
|
37
|
+
"<style>"
|
|
38
|
+
".cuiman-assets-card{border-radius:12px;box-shadow:0 3px 12px #0001}"
|
|
39
|
+
".cuiman-assets-map{border-radius:8px}"
|
|
40
|
+
".cuiman-assets{font:14px/1.5 system-ui,sans-serif;color:inherit}"
|
|
41
|
+
".cuiman-assets header{padding:0;"
|
|
42
|
+
"overflow-wrap:anywhere}"
|
|
43
|
+
".cuiman-assets header strong{font-size:20px;font-weight:650}"
|
|
44
|
+
".cuiman-assets-meta{opacity:.65;font-size:12px;margin-top:4px}"
|
|
45
|
+
".cuiman-assets-properties{display:grid;grid-template-columns:minmax(100px,auto) 1fr;"
|
|
46
|
+
"gap:4px 16px;margin:14px 0 0;font-size:12px;max-height:180px;overflow:auto}"
|
|
47
|
+
".cuiman-assets-properties dt{opacity:.6;font-weight:400}"
|
|
48
|
+
".cuiman-assets-properties dd{margin:0;overflow-wrap:anywhere}"
|
|
49
|
+
".cuiman-assets-table{overflow-x:auto;padding:8px 20px 16px}"
|
|
50
|
+
# Outrank notebook HTML table rules in both plain HTML and widget outputs.
|
|
51
|
+
".cuiman-assets .cuiman-assets-table table{width:100%;border-collapse:collapse;"
|
|
52
|
+
"text-align:left;background:transparent}"
|
|
53
|
+
".cuiman-assets .cuiman-assets-table table th{font-size:11px;text-transform:uppercase;"
|
|
54
|
+
"letter-spacing:.06em;opacity:.6;font-weight:600;text-align:left}"
|
|
55
|
+
".cuiman-assets .cuiman-assets-table table td,"
|
|
56
|
+
".cuiman-assets .cuiman-assets-table table th{padding:10px 12px;"
|
|
57
|
+
"border-bottom:1px solid #8882;text-align:left}"
|
|
58
|
+
".cuiman-assets .cuiman-assets-table table td:first-child{font-weight:600}"
|
|
59
|
+
".cuiman-assets .cuiman-assets-table table td:last-child,"
|
|
60
|
+
".cuiman-assets .cuiman-assets-table table th:last-child{width:32px;text-align:center}"
|
|
61
|
+
".cuiman-assets .cuiman-assets-table table tbody tr:last-child td{border-bottom:0}"
|
|
62
|
+
".cuiman-assets .cuiman-assets-table table tbody tr:nth-child(odd){background:#8881}"
|
|
63
|
+
".cuiman-assets .cuiman-assets-table table tbody tr:nth-child(even){background:transparent}"
|
|
64
|
+
".cuiman-assets .cuiman-assets-table table tbody tr:hover{background:#8882}"
|
|
65
|
+
".cuiman-assets a{color:#3b9fa5;text-decoration:none}"
|
|
66
|
+
".cuiman-assets a:hover{text-decoration:underline}"
|
|
67
|
+
".cuiman-assets-empty{padding:0 20px}"
|
|
68
|
+
".cuiman-assets-previews figure{margin:0;width:160px;min-width:0}"
|
|
69
|
+
".cuiman-assets-previews img{display:block;width:100%;"
|
|
70
|
+
"height:140px;object-fit:contain;background:#8881;border-radius:8px}"
|
|
71
|
+
".cuiman-assets-previews figcaption{font:12px/1.5 system-ui,sans-serif;"
|
|
72
|
+
"opacity:.65;padding:8px 2px}"
|
|
73
|
+
"</style>"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _render_link(href: str) -> str:
|
|
78
|
+
"""Render a file link, escaping labels and rejecting active URL schemes."""
|
|
79
|
+
path = urlsplit(href.replace("\\", "/")).path.rstrip("/")
|
|
80
|
+
filename = unquote(path.rsplit("/", 1)[-1]) or href
|
|
81
|
+
scheme = urlsplit(href).scheme.lower()
|
|
82
|
+
|
|
83
|
+
if scheme in ("", "http", "https", "file"):
|
|
84
|
+
return (
|
|
85
|
+
f"<a href='{escape(href, quote=True)}' "
|
|
86
|
+
"style='overflow-wrap:anywhere'>"
|
|
87
|
+
f"{escape(filename)}</a>"
|
|
88
|
+
)
|
|
89
|
+
else:
|
|
90
|
+
return escape(filename)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _copy_button(expression: str) -> str:
|
|
94
|
+
"""Render a button that copies an escaped Python access expression."""
|
|
95
|
+
return (
|
|
96
|
+
f"<button data-copy='{escape(expression, quote=True)}' "
|
|
97
|
+
"type='button' title='Copy expression' aria-label='Copy expression' "
|
|
98
|
+
"style='background:transparent;border:0;padding:0.25em;"
|
|
99
|
+
"color:inherit;cursor:pointer;line-height:0' "
|
|
100
|
+
'onclick="(async b => {'
|
|
101
|
+
"try { await navigator.clipboard.writeText(b.dataset.copy); "
|
|
102
|
+
"b.title = 'Copied'; } "
|
|
103
|
+
"catch (_) { window.prompt('Copy expression:', b.dataset.copy); }"
|
|
104
|
+
'})(this)">'
|
|
105
|
+
"<svg xmlns='http://www.w3.org/2000/svg' width='18' height='18' "
|
|
106
|
+
"viewBox='0 0 24 24' fill='none' stroke='currentColor' "
|
|
107
|
+
"stroke-width='2' stroke-linecap='round' stroke-linejoin='round' "
|
|
108
|
+
"aria-hidden='true' focusable='false'>"
|
|
109
|
+
"<rect x='9' y='9' width='13' height='13' rx='2'/>"
|
|
110
|
+
"<path d='M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9"
|
|
111
|
+
"a2 2 0 0 1 2 2v1'/></svg></button>"
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _get_items_expression(function_name: str = "show_assets") -> str | None:
|
|
116
|
+
"""Capture the public caller's items expression without retaining its frame."""
|
|
117
|
+
frame = currentframe()
|
|
118
|
+
try:
|
|
119
|
+
caller = frame.f_back.f_back if frame and frame.f_back else None
|
|
120
|
+
expression = _get_call_argument_source(caller, function_name)
|
|
121
|
+
finally:
|
|
122
|
+
del frame
|
|
123
|
+
if expression is None:
|
|
124
|
+
return None
|
|
125
|
+
|
|
126
|
+
import ast
|
|
127
|
+
|
|
128
|
+
node = ast.parse(f"({expression})", mode="eval").body
|
|
129
|
+
# Attribute access binds correctly to these expressions without parentheses.
|
|
130
|
+
return (
|
|
131
|
+
expression
|
|
132
|
+
if isinstance(node, (ast.Name, ast.Attribute, ast.Subscript, ast.Call))
|
|
133
|
+
else f"({expression})"
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _get_call_argument_source(
|
|
138
|
+
frame: FrameType | None, function_name: str
|
|
139
|
+
) -> str | None:
|
|
140
|
+
"""Recover the items argument from the caller's cached source, without eval.
|
|
141
|
+
|
|
142
|
+
Instruction positions distinguish calls on the same line and aliases.
|
|
143
|
+
When positions are unavailable, accept only one matching named call.
|
|
144
|
+
Missing source and ambiguous calls return None.
|
|
145
|
+
"""
|
|
146
|
+
import ast
|
|
147
|
+
import dis
|
|
148
|
+
import linecache
|
|
149
|
+
|
|
150
|
+
if frame is None:
|
|
151
|
+
return None
|
|
152
|
+
source = "".join(linecache.getlines(frame.f_code.co_filename, frame.f_globals))
|
|
153
|
+
try:
|
|
154
|
+
tree = ast.parse(source)
|
|
155
|
+
except SyntaxError:
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
positions = next(
|
|
159
|
+
(
|
|
160
|
+
instruction.positions
|
|
161
|
+
for instruction in dis.get_instructions(frame.f_code, show_caches=True)
|
|
162
|
+
if instruction.offset == frame.f_lasti
|
|
163
|
+
),
|
|
164
|
+
None,
|
|
165
|
+
)
|
|
166
|
+
position_span = (
|
|
167
|
+
tuple(positions)
|
|
168
|
+
if positions is not None and all(value is not None for value in positions)
|
|
169
|
+
else None
|
|
170
|
+
)
|
|
171
|
+
candidates = []
|
|
172
|
+
for node in ast.walk(tree):
|
|
173
|
+
if not isinstance(node, (ast.Call, ast.Await)):
|
|
174
|
+
continue
|
|
175
|
+
call = node.value if isinstance(node, ast.Await) else node
|
|
176
|
+
if not isinstance(call, ast.Call):
|
|
177
|
+
continue
|
|
178
|
+
if position_span is not None:
|
|
179
|
+
span = (node.lineno, node.end_lineno, node.col_offset, node.end_col_offset)
|
|
180
|
+
if span != position_span:
|
|
181
|
+
continue
|
|
182
|
+
else:
|
|
183
|
+
if not isinstance(node, ast.Call):
|
|
184
|
+
continue
|
|
185
|
+
name = (
|
|
186
|
+
call.func.id
|
|
187
|
+
if isinstance(call.func, ast.Name)
|
|
188
|
+
else call.func.attr
|
|
189
|
+
if isinstance(call.func, ast.Attribute)
|
|
190
|
+
else None
|
|
191
|
+
)
|
|
192
|
+
if name != function_name or not (
|
|
193
|
+
node.lineno <= frame.f_lineno <= (node.end_lineno or node.lineno)
|
|
194
|
+
):
|
|
195
|
+
continue
|
|
196
|
+
candidates.append(call)
|
|
197
|
+
|
|
198
|
+
if len(candidates) != 1:
|
|
199
|
+
return None
|
|
200
|
+
call = candidates[0]
|
|
201
|
+
argument = (
|
|
202
|
+
call.args[0]
|
|
203
|
+
if call.args
|
|
204
|
+
else next((kw.value for kw in call.keywords if kw.arg == "items"), None)
|
|
205
|
+
)
|
|
206
|
+
if argument is None or isinstance(argument, ast.Starred):
|
|
207
|
+
return None
|
|
208
|
+
return ast.get_source_segment(source, argument)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Copyright (c) 2026 by the Eozilla team and contributors
|
|
2
|
+
# Permissions are hereby granted under the terms of the Apache 2.0 License:
|
|
3
|
+
# https://opensource.org/license/apache-2-0.
|
|
4
|
+
|
|
5
|
+
"""Notebook tables for raw job outputs, without fetching linked resources."""
|
|
6
|
+
|
|
7
|
+
from html import escape
|
|
8
|
+
from json import dumps
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from gavicore.models.core import JobResults, Link, QualifiedValue
|
|
12
|
+
|
|
13
|
+
from .common import _copy_button, _render_link, _stac_label, _styles
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _render_value(value: Any) -> str:
|
|
17
|
+
"""Render JSON scalars directly and structured values as expandable JSON."""
|
|
18
|
+
if isinstance(value, (dict, list)):
|
|
19
|
+
return (
|
|
20
|
+
f"<details><summary>{_stac_label(value) or 'JSON'}</summary>"
|
|
21
|
+
"<pre style='max-height:320px;overflow:auto;white-space:pre-wrap'>"
|
|
22
|
+
f"{escape(dumps(value, ensure_ascii=False, indent=2))}</pre></details>"
|
|
23
|
+
)
|
|
24
|
+
return escape(dumps(value, ensure_ascii=False))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _render_job_results(results: JobResults, *, expression: str | None = None):
|
|
28
|
+
"""Build an output table with safe links and copyable root access expressions."""
|
|
29
|
+
from IPython.display import HTML
|
|
30
|
+
|
|
31
|
+
return HTML(_job_results_html(results, expression=expression))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _job_results_html(results: JobResults, *, expression: str | None = None) -> str:
|
|
35
|
+
"""Return the shared output table HTML without display or optional imports."""
|
|
36
|
+
rows = []
|
|
37
|
+
json_values = results.model_dump(mode="json", by_alias=True) or {}
|
|
38
|
+
for name, result in (results.root or {}).items():
|
|
39
|
+
media_type = ""
|
|
40
|
+
if isinstance(result, Link):
|
|
41
|
+
kind = "Link"
|
|
42
|
+
media_type = result.type or ""
|
|
43
|
+
value_html = _render_link(result.href)
|
|
44
|
+
elif isinstance(result, QualifiedValue):
|
|
45
|
+
kind = "Inline"
|
|
46
|
+
media_type = result.mediaType or ""
|
|
47
|
+
value_html = _render_value(json_values[name]["value"])
|
|
48
|
+
else:
|
|
49
|
+
kind = "Inline"
|
|
50
|
+
value_html = _render_value(json_values[name])
|
|
51
|
+
access = f"{expression or 'results'}.root[{name!r}]"
|
|
52
|
+
rows.append(
|
|
53
|
+
"<tr>"
|
|
54
|
+
f"<td>{escape(name)}</td><td>{kind}</td>"
|
|
55
|
+
f"<td>{escape(media_type)}</td><td>{value_html}</td>"
|
|
56
|
+
f"<td>{_copy_button(access)}</td></tr>"
|
|
57
|
+
)
|
|
58
|
+
table = (
|
|
59
|
+
"<div class='cuiman-assets-table'><table>"
|
|
60
|
+
"<thead><tr><th>Output</th><th>Kind</th><th>Media type</th>"
|
|
61
|
+
"<th>Value / location</th><th>Code</th></tr></thead>"
|
|
62
|
+
f"<tbody>{''.join(rows)}</tbody></table></div>"
|
|
63
|
+
if rows
|
|
64
|
+
else "<p class='cuiman-assets-empty'><em>No outputs</em></p>"
|
|
65
|
+
)
|
|
66
|
+
return (
|
|
67
|
+
_styles() + "<section class='cuiman-assets cuiman-assets-card' "
|
|
68
|
+
"style='border:1px solid #8884;overflow:hidden;margin:12px 0 24px'>"
|
|
69
|
+
"<header style='padding:16px 20px 8px'><strong>Job results</strong></header>"
|
|
70
|
+
f"{table}</section>"
|
|
71
|
+
)
|