ontobdc-view 0.2.0__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 (35) hide show
  1. ontobdc_view/__init__.py +236 -0
  2. ontobdc_view/component/__init__.py +0 -0
  3. ontobdc_view/component/adapter/component_source.py +127 -0
  4. ontobdc_view/component/asset/onto-csv-file-tile.js +373 -0
  5. ontobdc_view/component/asset/onto-data-container-tile.js +168 -0
  6. ontobdc_view/component/asset/onto-file-tree-tile.js +420 -0
  7. ontobdc_view/component/asset/onto-generic-file-tile.js +249 -0
  8. ontobdc_view/component/asset/onto-image-file-tile.js +247 -0
  9. ontobdc_view/component/asset/onto-language-tile.js +112 -0
  10. ontobdc_view/component/asset/onto-logo-tile.js +134 -0
  11. ontobdc_view/component/asset/onto-pdf-file-tile.js +261 -0
  12. ontobdc_view/component/asset/onto-photo-tile.js +180 -0
  13. ontobdc_view/component/asset/onto-presentation-surface.js +465 -0
  14. ontobdc_view/component/asset/onto-theme-tile.js +158 -0
  15. ontobdc_view/component/asset/onto-workstream-tile.js +386 -0
  16. ontobdc_view/component/plugin/__init__.py +0 -0
  17. ontobdc_view/component/plugin/csv_file_tile.py +24 -0
  18. ontobdc_view/component/plugin/data_container_tile.py +35 -0
  19. ontobdc_view/component/plugin/file_tree_tile.py +28 -0
  20. ontobdc_view/component/plugin/generic_file_tile.py +30 -0
  21. ontobdc_view/component/plugin/image_file_tile.py +24 -0
  22. ontobdc_view/component/plugin/language_tile.py +29 -0
  23. ontobdc_view/component/plugin/logo_tile.py +29 -0
  24. ontobdc_view/component/plugin/pdf_file_tile.py +24 -0
  25. ontobdc_view/component/plugin/photo_tile.py +31 -0
  26. ontobdc_view/component/plugin/theme_tile.py +29 -0
  27. ontobdc_view/component/plugin/workstream_tile.py +42 -0
  28. ontobdc_view/page/asset/work_stream_view.css +628 -0
  29. ontobdc_view/page/asset/work_stream_view.html.j2 +53 -0
  30. ontobdc_view/page/asset/work_stream_view.js +968 -0
  31. ontobdc_view/page/plugin/work_stream_view.py +30 -0
  32. ontobdc_view-0.2.0.dist-info/METADATA +183 -0
  33. ontobdc_view-0.2.0.dist-info/RECORD +35 -0
  34. ontobdc_view-0.2.0.dist-info/WHEEL +4 -0
  35. ontobdc_view-0.2.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,236 @@
1
+ from __future__ import annotations
2
+
3
+ import importlib
4
+ import inspect
5
+ import json
6
+ import pkgutil
7
+ from importlib.resources import files
8
+ from typing import Any, Dict, List, Optional
9
+
10
+ __all__ = [
11
+ "__version__",
12
+ "component_source",
13
+ "page_asset_root",
14
+ "page_asset_path",
15
+ "read_page_asset",
16
+ "render_entity_view",
17
+ ]
18
+
19
+ __version__ = "0.1.0"
20
+
21
+
22
+ def component_source(tag: str) -> Optional[str]:
23
+ """Return a Tile's ready-to-embed JS source by custom-element tag.
24
+
25
+ Delegates to `ComponentSourceAdapter` (`ontobdc.shared`'s
26
+ `ComponentSourcePort` implementation) — this function is just the
27
+ package's public entry point, matching how every other capability here
28
+ is exposed at the top level.
29
+ """
30
+ from .component.adapter.component_source import ComponentSourceAdapter
31
+
32
+ return ComponentSourceAdapter().component_source(tag)
33
+
34
+
35
+ def page_asset_root():
36
+ """Return the Traversable root containing Page HTML/CSS/JS assets."""
37
+ return files("ontobdc_view").joinpath("page", "asset")
38
+
39
+
40
+ def page_asset_path(name: str):
41
+ """Return a packaged Page asset by filename."""
42
+ return page_asset_root().joinpath(name)
43
+
44
+
45
+ def read_page_asset(name: str, *, encoding: str = "utf-8") -> str:
46
+ """Read a packaged Page asset (template/CSS/JS) as text."""
47
+ return page_asset_path(name).read_text(encoding=encoding)
48
+
49
+
50
+ def _iter_page_descriptors():
51
+ from ontobdc.shared.domain.port.page import PagePort
52
+
53
+ package = importlib.import_module("ontobdc_view.page.plugin")
54
+ package_prefix = f"{package.__name__}."
55
+ for _, name, _ in pkgutil.walk_packages(package.__path__, package_prefix):
56
+ module = importlib.import_module(name)
57
+ for _, obj in inspect.getmembers(module):
58
+ if not inspect.isclass(obj):
59
+ continue
60
+ try:
61
+ if not issubclass(obj, PagePort) or obj is PagePort:
62
+ continue
63
+ except TypeError:
64
+ continue
65
+ yield obj
66
+
67
+
68
+ def _matching_page_descriptor(entity_type_uris: List[str]):
69
+ type_set = set(entity_type_uris)
70
+ for descriptor in _iter_page_descriptors():
71
+ required = set(descriptor.METADATA.required_uris)
72
+ if required & type_set:
73
+ return descriptor
74
+ return None
75
+
76
+
77
+ _ANNOTATION_MODULE_LOAD_ORDER = (
78
+ "annotation_model.js",
79
+ "annotation_visual_contract.js",
80
+ "annotation_store.js",
81
+ "annotation_surface.js",
82
+ "annotation_visual_resolver.js",
83
+ "annotation_renderer.js",
84
+ "annotation_field_factory.js",
85
+ "annotation_form_registry.js",
86
+ "annotation_editor.js",
87
+ "annotation_geometry_controller.js",
88
+ "annotation_lifecycle.js",
89
+ "annotation_workspace.js",
90
+ "annotation_subject_page.js",
91
+ "annotation_runtime.js",
92
+ )
93
+
94
+
95
+ def _annotation_runtime_script() -> Optional[str]:
96
+ """Concatenate ontobdc's native annotation modules, in dependency order.
97
+
98
+ These live in `ontobdc` (`view/plugin/asset/js/annotation/`), not here
99
+ — `ontobdc_view` already depends on `ontobdc`, so reading its packaged
100
+ JS assets to inline them into a Page is a legitimate use of that
101
+ dependency, not a layering violation. Returns None if the modules
102
+ aren't installed/found, so a Page can render without annotations
103
+ rather than fail outright.
104
+ """
105
+ try:
106
+ root = files("ontobdc").joinpath("view", "plugin", "asset", "js", "annotation")
107
+ parts = [
108
+ root.joinpath(name).read_text(encoding="utf-8")
109
+ for name in _ANNOTATION_MODULE_LOAD_ORDER
110
+ ]
111
+ except (FileNotFoundError, OSError, ModuleNotFoundError):
112
+ return None
113
+ return "\n".join(parts)
114
+
115
+
116
+ _ANNOTATION_CSS_FILES = (
117
+ "annotation_visual.css",
118
+ "annotation_editor.css",
119
+ "annotation_workspace.css",
120
+ )
121
+
122
+
123
+ def _annotation_runtime_style() -> str:
124
+ try:
125
+ root = files("ontobdc").joinpath("view", "plugin", "asset", "css")
126
+ parts = [
127
+ root.joinpath(name).read_text(encoding="utf-8")
128
+ for name in _ANNOTATION_CSS_FILES
129
+ ]
130
+ except (FileNotFoundError, OSError, ModuleNotFoundError):
131
+ return ""
132
+ return "\n".join(parts)
133
+
134
+
135
+ def _workstream_context_script() -> str:
136
+ """Read ontobdc's workstream_context.js (dimension URI helper).
137
+
138
+ Same cross-package-read rationale as `_annotation_runtime_script()`.
139
+ """
140
+ try:
141
+ return (
142
+ files("ontobdc")
143
+ .joinpath("view", "plugin", "asset", "js", "workstream", "workstream_context.js")
144
+ .read_text(encoding="utf-8")
145
+ )
146
+ except (FileNotFoundError, OSError, ModuleNotFoundError):
147
+ return ""
148
+
149
+
150
+ def _escape_for_script_embedding(json_text: str) -> str:
151
+ # A literal `</script>` inside the JSON payload would otherwise close
152
+ # the embedding <script> tag early — same concern/fix as the Surface's
153
+ # own JSON-LD embedding.
154
+ return json_text.replace("</", "<\\/")
155
+
156
+
157
+ _DCTERMS_IDENTIFIER = "http://purl.org/dc/terms/identifier"
158
+
159
+
160
+ def _literal(entity_data: dict, property_uri: str) -> str:
161
+ values = entity_data.get(property_uri)
162
+ if not isinstance(values, list) or not values:
163
+ return ""
164
+ picked = values[0]
165
+ if isinstance(picked, dict):
166
+ return str(picked.get("@value") or picked.get("@id") or "").strip()
167
+ return str(picked).strip()
168
+
169
+
170
+ def _resolve_identifier(entity_data: dict) -> str:
171
+ # Same fallback order as each Tile's own client-side #literal()
172
+ # resolution, so the file this writes to matches the URL a Tile's
173
+ # "Open" link already constructs.
174
+ return _literal(entity_data, _DCTERMS_IDENTIFIER) or str(entity_data.get("@id", ""))
175
+
176
+
177
+ def render_entity_view(
178
+ entity_type_uris: List[str],
179
+ entity_data: dict,
180
+ *,
181
+ graph_nodes: Optional[List[dict]] = None,
182
+ language: str = "en",
183
+ ) -> Optional[Dict[str, str]]:
184
+ """Render a standalone detail Page for one entity.
185
+
186
+ Returns `None` if no Page descriptor's `required_uris` matches any of
187
+ `entity_type_uris` — the caller (ontobdc's orchestration capability)
188
+ should skip that entity in that case, which is what makes this
189
+ mechanism generic across entity types without any per-type code on the
190
+ `ontobdc` side.
191
+
192
+ Otherwise returns `{"html": ..., "path_segment": ..., "identifier": ...}`
193
+ — the caller writes `html` to `view/<path_segment>/<identifier>.html`.
194
+ All of that routing knowledge stays here, not in `ontobdc`.
195
+
196
+ `entity_data` is the entity's own JSON-LD node. `graph_nodes` (defaults
197
+ to just `[entity_data]` when omitted) is the wider Surface graph the
198
+ Page may need to build a resource list from (e.g. the WorkStream Page's
199
+ Related/Found tree) — embedded verbatim for the Page's own client-side
200
+ script to read, mirroring how Tiles read the Surface-wide JSON-LD.
201
+ """
202
+ descriptor = _matching_page_descriptor(entity_type_uris)
203
+ if descriptor is None:
204
+ return None
205
+
206
+ from jinja2 import Template
207
+
208
+ metadata = descriptor.METADATA
209
+ base_name = metadata.template.removesuffix(".html.j2")
210
+ template_text = read_page_asset(metadata.template)
211
+ css_content = read_page_asset(f"{base_name}.css")
212
+ js_content = read_page_asset(f"{base_name}.js")
213
+
214
+ entity_id = str(entity_data.get("@id", ""))
215
+ nodes = graph_nodes if graph_nodes is not None else [entity_data]
216
+ entity_json = _escape_for_script_embedding(
217
+ json.dumps(nodes, ensure_ascii=False)
218
+ )
219
+
220
+ html = Template(template_text).render(
221
+ language=language,
222
+ page_title=metadata.name,
223
+ entity_id=entity_id,
224
+ entity_json=entity_json,
225
+ css_content=css_content,
226
+ js_content=js_content,
227
+ annotation_runtime_js=_annotation_runtime_script() or "",
228
+ annotation_runtime_css=_annotation_runtime_style(),
229
+ workstream_context_js=_workstream_context_script(),
230
+ )
231
+
232
+ return {
233
+ "html": html,
234
+ "path_segment": metadata.path_segment,
235
+ "identifier": _resolve_identifier(entity_data),
236
+ }
File without changes
@@ -0,0 +1,127 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from importlib.resources import files
5
+ from typing import Optional
6
+ from urllib.parse import quote
7
+
8
+ from ontobdc.shared.domain.port.component_source import ComponentSourcePort
9
+
10
+ _BUILD_PLACEHOLDER = "__ONTOBDC_BUILD_"
11
+
12
+ _THEMES = [
13
+ {
14
+ "name": "light",
15
+ "label": "Light",
16
+ "tokens": {
17
+ "background": "#ffffff",
18
+ "foreground": "#0f172a",
19
+ "accent": "#0284c7",
20
+ "borderColor": "rgba(2,132,199,.35)",
21
+ },
22
+ "options": {"transparentBackground": False},
23
+ },
24
+ {
25
+ "name": "dark",
26
+ "label": "Dark",
27
+ "tokens": {
28
+ "background": "#000000",
29
+ "foreground": "#f8fafc",
30
+ "accent": "#38bdf8",
31
+ "borderColor": "rgba(56,189,248,.55)",
32
+ },
33
+ "options": {"transparentBackground": False},
34
+ },
35
+ ]
36
+
37
+ _BRAND = {
38
+ "name": "OntoBDC",
39
+ "mark_svg": (
40
+ '<svg viewBox="0 0 64 64" aria-hidden="true">'
41
+ '<circle cx="32" cy="32" r="25" fill="none" stroke="currentColor" stroke-width="8"/>'
42
+ '<circle cx="32" cy="32" r="7" fill="currentColor"/></svg>'
43
+ ),
44
+ "logotype_svg": (
45
+ '<svg viewBox="0 0 260 64" aria-hidden="true">'
46
+ '<circle cx="32" cy="32" r="23" fill="none" stroke="var(--onto-theme-accent, currentColor)" stroke-width="7"/>'
47
+ '<circle cx="32" cy="32" r="6" fill="var(--onto-theme-accent, currentColor)"/>'
48
+ '<text x="68" y="42" fill="currentColor" font-family="system-ui, sans-serif" '
49
+ 'font-size="31" font-weight="700">OntoBDC</text></svg>'
50
+ ),
51
+ "slogan": "Data with Brains",
52
+ }
53
+
54
+ _LANGUAGES = [
55
+ {"code": "pt-BR", "label": "Portugues", "short_label": "PT"},
56
+ {"code": "en", "label": "English", "short_label": "EN"},
57
+ {"code": "es", "label": "Espanol", "short_label": "ES"},
58
+ ]
59
+
60
+
61
+ def _stub_photo_data_uri() -> str:
62
+ svg = (
63
+ "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1200 800'>"
64
+ "<rect width='1200' height='800' fill='#0f172a'/>"
65
+ "<circle cx='785' cy='285' r='95' fill='#38bdf8' opacity='.85'/>"
66
+ "<text x='120' y='170' fill='white' font-family='system-ui,sans-serif' "
67
+ "font-size='58' font-weight='700'>OntoBDC</text>"
68
+ "</svg>"
69
+ )
70
+ return "data:image/svg+xml;charset=UTF-8," + quote(svg)
71
+
72
+
73
+ _PHOTO = {
74
+ "id": "stub-photo",
75
+ "data_uri": _stub_photo_data_uri(),
76
+ "alt": "OntoBDC placeholder photo",
77
+ "caption": "",
78
+ "author": "",
79
+ "date": "",
80
+ "location": "",
81
+ "mode": "photo",
82
+ "fit": "cover",
83
+ "focal_x": 0.5,
84
+ "focal_y": 0.5,
85
+ }
86
+
87
+ _BUILD_PAYLOAD_BY_PLACEHOLDER = {
88
+ "__ONTOBDC_BUILD_THEMES__": _THEMES,
89
+ "__ONTOBDC_BUILD_BRAND__": _BRAND,
90
+ "__ONTOBDC_BUILD_LANGUAGES__": _LANGUAGES,
91
+ "__ONTOBDC_BUILD_PHOTO__": _PHOTO,
92
+ }
93
+
94
+
95
+ class ComponentSourceAdapter(ComponentSourcePort):
96
+ """Reads a Tile's JS asset and resolves its build-time placeholder, if any.
97
+
98
+ A plain content Tile's JS has no placeholder and is returned as-is. A
99
+ "system" Tile's JS (logo/theme/language/photo) carries exactly one of
100
+ the placeholders in `_BUILD_PAYLOAD_BY_PLACEHOLDER`, substituted here
101
+ with project-default data — there is currently no per-project
102
+ customization path, since nothing in the active pipeline ever supplied
103
+ anything other than these defaults.
104
+ """
105
+
106
+ def component_source(self, tag: str) -> Optional[str]:
107
+ try:
108
+ source = files("ontobdc_view").joinpath(
109
+ "component", "asset", f"{tag}.js"
110
+ ).read_text(encoding="utf-8")
111
+ except (FileNotFoundError, OSError):
112
+ return None
113
+
114
+ if not source.strip():
115
+ return None
116
+
117
+ for placeholder, payload in _BUILD_PAYLOAD_BY_PLACEHOLDER.items():
118
+ if placeholder in source:
119
+ encoded = json.dumps(payload, ensure_ascii=False, separators=(",", ":"))
120
+ return source.replace(placeholder, encoded)
121
+
122
+ if _BUILD_PLACEHOLDER in source:
123
+ # An unresolved placeholder we don't recognize — embedding it
124
+ # as-is would ship broken JS.
125
+ return None
126
+
127
+ return source