tangram-core 0.2.0__cp313-cp313-win_amd64.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.
- tangram_core/App.vue +335 -0
- tangram_core/__init__.py +5 -0
- tangram_core/__main__.py +141 -0
- tangram_core/_core.cp313-win_amd64.pyd +0 -0
- tangram_core/_core.pyi +38 -0
- tangram_core/api.ts +456 -0
- tangram_core/backend.py +335 -0
- tangram_core/colour.ts +71 -0
- tangram_core/config.py +122 -0
- tangram_core/dist-frontend/aggregation-layers.js +521 -0
- tangram_core/dist-frontend/aggregation-layers.js.map +1 -0
- tangram_core/dist-frontend/assets/_commonjsHelpers-CqkleIqs.js +2 -0
- tangram_core/dist-frontend/assets/_commonjsHelpers-CqkleIqs.js.map +1 -0
- tangram_core/dist-frontend/assets/array-utils-flat-wyE8tIYR.js +11 -0
- tangram_core/dist-frontend/assets/array-utils-flat-wyE8tIYR.js.map +1 -0
- tangram_core/dist-frontend/assets/assert-hrfsarFU.js +3 -0
- tangram_core/dist-frontend/assets/assert-hrfsarFU.js.map +1 -0
- tangram_core/dist-frontend/assets/b612-latin-400-italic-DePNXA0a.woff +0 -0
- tangram_core/dist-frontend/assets/b612-latin-400-italic-a-4GLPtl.woff2 +0 -0
- tangram_core/dist-frontend/assets/b612-latin-400-normal-CC98FVm_.woff2 +0 -0
- tangram_core/dist-frontend/assets/b612-latin-400-normal-JbZ7xwUX.woff +0 -0
- tangram_core/dist-frontend/assets/b612-latin-700-normal-B_Snq1wd.woff +0 -0
- tangram_core/dist-frontend/assets/b612-latin-700-normal-BinQrnoB.woff2 +0 -0
- tangram_core/dist-frontend/assets/clip-extension-DTCP51Ak.js +26 -0
- tangram_core/dist-frontend/assets/clip-extension-DTCP51Ak.js.map +1 -0
- tangram_core/dist-frontend/assets/color-CUNNsFV-.js +17 -0
- tangram_core/dist-frontend/assets/color-CUNNsFV-.js.map +1 -0
- tangram_core/dist-frontend/assets/cube-geometry-CzJ_uBWa.js +2 -0
- tangram_core/dist-frontend/assets/cube-geometry-CzJ_uBWa.js.map +1 -0
- tangram_core/dist-frontend/assets/deep-equal-uriyKJca.js +2 -0
- tangram_core/dist-frontend/assets/deep-equal-uriyKJca.js.map +1 -0
- tangram_core/dist-frontend/assets/fly-to-interpolator-DlKiy9_S.js +2 -0
- tangram_core/dist-frontend/assets/fly-to-interpolator-DlKiy9_S.js.map +1 -0
- tangram_core/dist-frontend/assets/geojson-layer-CLhXLxdI.js +1010 -0
- tangram_core/dist-frontend/assets/geojson-layer-CLhXLxdI.js.map +1 -0
- tangram_core/dist-frontend/assets/globe-view-DKhftlA1.js +94 -0
- tangram_core/dist-frontend/assets/globe-view-DKhftlA1.js.map +1 -0
- tangram_core/dist-frontend/assets/globe-viewport-CPES4D4P.js +2 -0
- tangram_core/dist-frontend/assets/globe-viewport-CPES4D4P.js.map +1 -0
- tangram_core/dist-frontend/assets/image-loader-ClbNCMXW.js +2 -0
- tangram_core/dist-frontend/assets/image-loader-ClbNCMXW.js.map +1 -0
- tangram_core/dist-frontend/assets/inconsolata-latin-400-normal-DTZQ6lD6.woff2 +0 -0
- tangram_core/dist-frontend/assets/inconsolata-latin-400-normal-HYADljCo.woff +0 -0
- tangram_core/dist-frontend/assets/inconsolata-latin-700-normal-ByjKuJjN.woff2 +0 -0
- tangram_core/dist-frontend/assets/inconsolata-latin-700-normal-DzgUY3Rl.woff +0 -0
- tangram_core/dist-frontend/assets/inconsolata-latin-ext-400-normal-BaHVOdFB.woff2 +0 -0
- tangram_core/dist-frontend/assets/inconsolata-latin-ext-400-normal-yvPjCxxx.woff +0 -0
- tangram_core/dist-frontend/assets/inconsolata-latin-ext-700-normal-D0Kpgs_9.woff2 +0 -0
- tangram_core/dist-frontend/assets/inconsolata-latin-ext-700-normal-Dlt-daqV.woff +0 -0
- tangram_core/dist-frontend/assets/inconsolata-vietnamese-400-normal-ByiM2lek.woff +0 -0
- tangram_core/dist-frontend/assets/inconsolata-vietnamese-400-normal-DfC_iMic.woff2 +0 -0
- tangram_core/dist-frontend/assets/inconsolata-vietnamese-700-normal-DLCFFAUf.woff +0 -0
- tangram_core/dist-frontend/assets/inconsolata-vietnamese-700-normal-DuasYmn8.woff2 +0 -0
- tangram_core/dist-frontend/assets/index-UPPakSLR.css +1 -0
- tangram_core/dist-frontend/assets/index-r8T0kY2p.js +821 -0
- tangram_core/dist-frontend/assets/index-r8T0kY2p.js.map +1 -0
- tangram_core/dist-frontend/assets/layer-DO63TrsS.js +555 -0
- tangram_core/dist-frontend/assets/layer-DO63TrsS.js.map +1 -0
- tangram_core/dist-frontend/assets/layer-extension-CZ3zsHuN.js +2 -0
- tangram_core/dist-frontend/assets/layer-extension-CZ3zsHuN.js.map +1 -0
- tangram_core/dist-frontend/assets/mesh-layers-BSECKarm.js +1123 -0
- tangram_core/dist-frontend/assets/mesh-layers-BSECKarm.js.map +1 -0
- tangram_core/dist-frontend/assets/orthographic-viewport-CzZmHDEZ.js +2 -0
- tangram_core/dist-frontend/assets/orthographic-viewport-CzZmHDEZ.js.map +1 -0
- tangram_core/dist-frontend/assets/pick-layers-pass-xhWsgZtf.js +2 -0
- tangram_core/dist-frontend/assets/pick-layers-pass-xhWsgZtf.js.map +1 -0
- tangram_core/dist-frontend/assets/project-CrvReKGW.js +760 -0
- tangram_core/dist-frontend/assets/project-CrvReKGW.js.map +1 -0
- tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-400-italic-4qS3_zkX.woff2 +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-400-italic-CDK-EZBY.woff +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-400-normal-Bgns473E.woff +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-400-normal-_T2aQlWs.woff2 +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-500-normal-CvEVpWxD.woff +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-500-normal-s4PklZE0.woff2 +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-700-normal-9RN-Z7cI.woff2 +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-700-normal-BGMkBBYx.woff +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-ext-400-italic-C7erd-g8.woff +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-ext-400-italic-DR5R5TWx.woff2 +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-ext-400-normal-DGo1Ayjq.woff2 +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-ext-400-normal-WtM1l1qc.woff +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-ext-500-normal-C8FNIdXm.woff2 +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-ext-500-normal-TLDmfi3Q.woff +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-ext-700-normal-CTXjXnze.woff2 +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-ext-700-normal-CWPRiRXS.woff +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-greek-400-italic-CR6qj4Z4.woff2 +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-greek-400-italic-DHRaIs10.woff +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-greek-400-normal-D5vBSIyg.woff2 +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-greek-400-normal-FabMgVmk.woff +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-greek-500-normal-BIN62cw9.woff +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-greek-500-normal-Hsn-wDIp.woff2 +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-greek-700-normal-89Up2Xly.woff +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-greek-700-normal-DWMOA2VK.woff2 +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-latin-400-italic-D_BR-3LG.woff2 +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-latin-400-italic-om57GXsO.woff +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-latin-400-normal-BICmKrXV.woff2 +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-latin-400-normal-D2e7XwB1.woff +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-latin-500-normal-3p2daRJW.woff2 +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-latin-500-normal-Dc9bsamC.woff +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-latin-700-normal-BOl6B_hI.woff +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-latin-700-normal-DRbp0YnP.woff2 +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-latin-ext-400-italic-BXrkWnoY.woff +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-latin-ext-400-italic-Bhem1d5z.woff2 +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-latin-ext-400-normal-DT8nEsYA.woff +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-latin-ext-400-normal-OHaX69iP.woff2 +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-latin-ext-500-normal-CcSTXKtO.woff2 +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-latin-ext-500-normal-JgPl2bDS.woff +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-latin-ext-700-normal-B004qtqu.woff2 +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-latin-ext-700-normal-O6H_RRvN.woff +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-vietnamese-400-italic-BwUYFJ2t.woff2 +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-vietnamese-400-italic-DV8QogUk.woff +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-vietnamese-400-normal-0o1laQ-g.woff2 +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-vietnamese-400-normal-CPsdS8_S.woff +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-vietnamese-500-normal-G9shSJ2z.woff +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-vietnamese-500-normal-TFWhjk13.woff2 +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-vietnamese-700-normal-BtNeb9D6.woff +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-vietnamese-700-normal-D35V1G0s.woff2 +0 -0
- tangram_core/dist-frontend/assets/shader-BJmsOfPx.js +843 -0
- tangram_core/dist-frontend/assets/shader-BJmsOfPx.js.map +1 -0
- tangram_core/dist-frontend/assets/solid-polygon-layer-DiarVGxh.js +392 -0
- tangram_core/dist-frontend/assets/solid-polygon-layer-DiarVGxh.js.map +1 -0
- tangram_core/dist-frontend/assets/tesselator-49Dw9L5A.js +2 -0
- tangram_core/dist-frontend/assets/tesselator-49Dw9L5A.js.map +1 -0
- tangram_core/dist-frontend/assets/webgl-developer-tools-CZl8qVFg.js +7 -0
- tangram_core/dist-frontend/assets/webgl-developer-tools-CZl8qVFg.js.map +1 -0
- tangram_core/dist-frontend/assets/webgl-device-BY0-CUP6.js +3 -0
- tangram_core/dist-frontend/assets/webgl-device-BY0-CUP6.js.map +1 -0
- tangram_core/dist-frontend/assets/widget-BbOeHGj0.js +2 -0
- tangram_core/dist-frontend/assets/widget-BbOeHGj0.js.map +1 -0
- tangram_core/dist-frontend/core.js +60 -0
- tangram_core/dist-frontend/core.js.map +1 -0
- tangram_core/dist-frontend/extensions.js +609 -0
- tangram_core/dist-frontend/extensions.js.map +1 -0
- tangram_core/dist-frontend/favicon.ico +0 -0
- tangram_core/dist-frontend/favicon.png +0 -0
- tangram_core/dist-frontend/font-awesome.min.css +4 -0
- tangram_core/dist-frontend/fonts/FontAwesome.otf +0 -0
- tangram_core/dist-frontend/fonts/fontawesome-webfont.eot +0 -0
- tangram_core/dist-frontend/fonts/fontawesome-webfont.svg +2671 -0
- tangram_core/dist-frontend/fonts/fontawesome-webfont.ttf +0 -0
- tangram_core/dist-frontend/fonts/fontawesome-webfont.woff +0 -0
- tangram_core/dist-frontend/fonts/fontawesome-webfont.woff2 +0 -0
- tangram_core/dist-frontend/geo-layers.js +115 -0
- tangram_core/dist-frontend/geo-layers.js.map +1 -0
- tangram_core/dist-frontend/index.html +38 -0
- tangram_core/dist-frontend/json.js +3 -0
- tangram_core/dist-frontend/json.js.map +1 -0
- tangram_core/dist-frontend/layers.js +268 -0
- tangram_core/dist-frontend/layers.js.map +1 -0
- tangram_core/dist-frontend/lit-html.js +7 -0
- tangram_core/dist-frontend/lit-html.js.map +1 -0
- tangram_core/dist-frontend/mapbox.js +2 -0
- tangram_core/dist-frontend/mapbox.js.map +1 -0
- tangram_core/dist-frontend/maplibre-gl.js +59 -0
- tangram_core/dist-frontend/maplibre-gl.js.map +1 -0
- tangram_core/dist-frontend/mesh-layers.js +2 -0
- tangram_core/dist-frontend/mesh-layers.js.map +1 -0
- tangram_core/dist-frontend/rs1090_wasm.js +813 -0
- tangram_core/dist-frontend/rs1090_wasm_bg.wasm +0 -0
- tangram_core/dist-frontend/vue.esm-browser.prod.js +13 -0
- tangram_core/dist-frontend/widgets.js +3 -0
- tangram_core/dist-frontend/widgets.js.map +1 -0
- tangram_core/main.ts +16 -0
- tangram_core/plugin.py +70 -0
- tangram_core/plugin.ts +41 -0
- tangram_core/redis.py +89 -0
- tangram_core/user.css +114 -0
- tangram_core/vite-plugin-tangram.mjs +88 -0
- tangram_core-0.2.0.dist-info/METADATA +37 -0
- tangram_core-0.2.0.dist-info/RECORD +171 -0
- tangram_core-0.2.0.dist-info/WHEEL +4 -0
- tangram_core-0.2.0.dist-info/entry_points.txt +2 -0
tangram_core/backend.py
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import importlib.resources
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import re
|
|
8
|
+
from contextlib import AsyncExitStack, asynccontextmanager
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from datetime import datetime, timezone
|
|
11
|
+
from functools import partial
|
|
12
|
+
from importlib.metadata import Distribution, PackageNotFoundError
|
|
13
|
+
from importlib.resources.abc import Traversable
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import (
|
|
16
|
+
TYPE_CHECKING,
|
|
17
|
+
Annotated,
|
|
18
|
+
Any,
|
|
19
|
+
AsyncGenerator,
|
|
20
|
+
Awaitable,
|
|
21
|
+
Callable,
|
|
22
|
+
Iterable,
|
|
23
|
+
TypeAlias,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
import httpx
|
|
27
|
+
import redis.asyncio as redis
|
|
28
|
+
import uvicorn
|
|
29
|
+
from fastapi import Depends, FastAPI, Request
|
|
30
|
+
from fastapi.responses import FileResponse, JSONResponse
|
|
31
|
+
from fastapi.staticfiles import StaticFiles
|
|
32
|
+
from platformdirs import user_cache_dir
|
|
33
|
+
|
|
34
|
+
from .config import CacheEntry, Config, FrontendChannelConfig, FrontendConfig
|
|
35
|
+
from .plugin import load_plugin, scan_plugins
|
|
36
|
+
|
|
37
|
+
if TYPE_CHECKING:
|
|
38
|
+
from .plugin import DistName, Plugin
|
|
39
|
+
|
|
40
|
+
logger = logging.getLogger(__name__)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# see https://www.starlette.io/lifespan/#lifespan-state
|
|
44
|
+
@dataclass
|
|
45
|
+
class BackendState:
|
|
46
|
+
redis_client: redis.Redis
|
|
47
|
+
http_client: httpx.AsyncClient
|
|
48
|
+
config: Config
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
async def get_state(request: Request) -> BackendState:
|
|
52
|
+
return request.app.state.backend_state # type: ignore
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
InjectBackendState: TypeAlias = Annotated[BackendState, Depends(get_state)]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def resolve_frontend(*, path: str, dist_name: str) -> Path | Traversable | None:
|
|
59
|
+
# always try to parse from direct url first (this is robust for editable
|
|
60
|
+
# installs like `uv sync --all-packages`)
|
|
61
|
+
try:
|
|
62
|
+
dist = Distribution.from_name(dist_name)
|
|
63
|
+
if direct_url_content := dist.read_text("direct_url.json"):
|
|
64
|
+
direct_url_data = json.loads(direct_url_content)
|
|
65
|
+
if (url := direct_url_data.get("url")) and (
|
|
66
|
+
path1 := Path(url.removeprefix("file://")) / path
|
|
67
|
+
).is_dir():
|
|
68
|
+
return path1
|
|
69
|
+
except (PackageNotFoundError, json.JSONDecodeError, FileNotFoundError):
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
# fallback in case it was installed via pip
|
|
73
|
+
if (path2 := importlib.resources.files(dist_name) / path).is_dir():
|
|
74
|
+
return path2
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def load_enabled_plugins(
|
|
79
|
+
config: Config,
|
|
80
|
+
) -> list[tuple[DistName, Plugin]]:
|
|
81
|
+
loaded_plugins = []
|
|
82
|
+
enabled_plugin_names = set(config.core.plugins)
|
|
83
|
+
|
|
84
|
+
for entry_point in scan_plugins():
|
|
85
|
+
if entry_point.name not in enabled_plugin_names:
|
|
86
|
+
continue
|
|
87
|
+
if (plugin := load_plugin(entry_point)) is not None:
|
|
88
|
+
loaded_plugins.append(plugin)
|
|
89
|
+
|
|
90
|
+
return loaded_plugins
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@asynccontextmanager
|
|
94
|
+
async def lifespan(
|
|
95
|
+
app: FastAPI, backend_state: BackendState
|
|
96
|
+
) -> AsyncGenerator[None, None]:
|
|
97
|
+
# we don't need to __aenter__ httpx.AsyncClient again
|
|
98
|
+
async with backend_state.redis_client:
|
|
99
|
+
app.state.backend_state = backend_state
|
|
100
|
+
yield
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def make_cache_route_handler(
|
|
104
|
+
entry: CacheEntry, state: BackendState
|
|
105
|
+
) -> Callable[..., Awaitable[FileResponse]]:
|
|
106
|
+
"""
|
|
107
|
+
Factory function that creates a route handler for caching and serving files.
|
|
108
|
+
Dynamically handles URL parameters found in both serve_route and origin.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
entry: Cache entry configuration
|
|
112
|
+
state: Backend state with http_client for fetching remote resources
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
Async function that handles the route with dynamic parameters
|
|
116
|
+
"""
|
|
117
|
+
from inspect import Parameter, Signature
|
|
118
|
+
|
|
119
|
+
# Extract parameter names from the serve_route (e.g., {fontstack}, {range})
|
|
120
|
+
param_pattern = re.compile(r"\{(\w+)\}")
|
|
121
|
+
params = param_pattern.findall(entry.serve_route)
|
|
122
|
+
|
|
123
|
+
async def cache_route_handler(**kwargs: str) -> FileResponse:
|
|
124
|
+
local_path = entry.local_path
|
|
125
|
+
if local_path is None:
|
|
126
|
+
local_path = Path(user_cache_dir("tangram_core"))
|
|
127
|
+
if not local_path.exists():
|
|
128
|
+
local_path.mkdir(parents=True)
|
|
129
|
+
else:
|
|
130
|
+
local_path = local_path.expanduser()
|
|
131
|
+
|
|
132
|
+
# Build the local file path by replacing parameters
|
|
133
|
+
local_file = local_path
|
|
134
|
+
for param in params:
|
|
135
|
+
if param in kwargs:
|
|
136
|
+
local_file = local_file / kwargs[param]
|
|
137
|
+
|
|
138
|
+
logger.info(f"Serving cached file from {local_file}")
|
|
139
|
+
|
|
140
|
+
if not local_file.exists():
|
|
141
|
+
assert entry.origin is not None
|
|
142
|
+
# Build the remote URL by replacing parameters
|
|
143
|
+
remote_url = entry.origin
|
|
144
|
+
for param, value in kwargs.items():
|
|
145
|
+
remote_url = remote_url.replace(f"{{{param}}}", value)
|
|
146
|
+
|
|
147
|
+
logger.info(f"Downloading from {remote_url} to {local_file}")
|
|
148
|
+
c = await state.http_client.get(remote_url)
|
|
149
|
+
c.raise_for_status()
|
|
150
|
+
local_file.parent.mkdir(parents=True, exist_ok=True)
|
|
151
|
+
local_file.write_bytes(c.content)
|
|
152
|
+
|
|
153
|
+
return FileResponse(path=local_file, media_type=entry.media_type)
|
|
154
|
+
|
|
155
|
+
# Create explicit parameters for the function signature
|
|
156
|
+
sig_params = [
|
|
157
|
+
Parameter(
|
|
158
|
+
name=param,
|
|
159
|
+
kind=Parameter.POSITIONAL_OR_KEYWORD,
|
|
160
|
+
annotation=str,
|
|
161
|
+
)
|
|
162
|
+
for param in params
|
|
163
|
+
]
|
|
164
|
+
cache_route_handler.__signature__ = Signature( # type: ignore
|
|
165
|
+
parameters=sig_params,
|
|
166
|
+
return_annotation=FileResponse,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
return cache_route_handler
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def create_app(
|
|
173
|
+
backend_state: BackendState,
|
|
174
|
+
loaded_plugins: Iterable[tuple[DistName, Plugin]],
|
|
175
|
+
) -> FastAPI:
|
|
176
|
+
app = FastAPI(lifespan=partial(lifespan, backend_state=backend_state))
|
|
177
|
+
frontend_plugins = []
|
|
178
|
+
|
|
179
|
+
for dist_name, plugin in loaded_plugins:
|
|
180
|
+
for router in plugin.routers:
|
|
181
|
+
app.include_router(router)
|
|
182
|
+
|
|
183
|
+
if (p := plugin.frontend_path) is not None and (
|
|
184
|
+
frontend_path_resolved := resolve_frontend(path=p, dist_name=dist_name)
|
|
185
|
+
) is not None:
|
|
186
|
+
app.mount(
|
|
187
|
+
f"/plugins/{dist_name}",
|
|
188
|
+
StaticFiles(directory=str(frontend_path_resolved)),
|
|
189
|
+
name=dist_name,
|
|
190
|
+
)
|
|
191
|
+
frontend_plugins.append(dist_name)
|
|
192
|
+
|
|
193
|
+
# unlike v0.1 which uses `process.env`, v0.2 *compiles* the js so we no
|
|
194
|
+
# no longer have access to it, so we selectively forward the config.
|
|
195
|
+
@app.get("/config")
|
|
196
|
+
async def get_frontend_config(
|
|
197
|
+
state: Annotated[BackendState, Depends(get_state)],
|
|
198
|
+
) -> FrontendConfig:
|
|
199
|
+
channel_cfg = state.config.channel
|
|
200
|
+
if channel_cfg.public_url:
|
|
201
|
+
channel_url = channel_cfg.public_url
|
|
202
|
+
else:
|
|
203
|
+
# for local/non-proxied setups, user must set a reachable host.
|
|
204
|
+
# '0.0.0.0' is for listening, not connecting.
|
|
205
|
+
host = "localhost" if channel_cfg.host == "0.0.0.0" else channel_cfg.host
|
|
206
|
+
channel_url = f"http://{host}:{channel_cfg.port}"
|
|
207
|
+
|
|
208
|
+
return FrontendConfig(
|
|
209
|
+
channel=FrontendChannelConfig(url=channel_url),
|
|
210
|
+
map=state.config.map,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
@app.get("/manifest.json")
|
|
214
|
+
async def get_manifest() -> JSONResponse:
|
|
215
|
+
return JSONResponse(content={"plugins": frontend_plugins})
|
|
216
|
+
|
|
217
|
+
# Cache mechanism - MUST be registered BEFORE the catch-all frontend mount
|
|
218
|
+
for cache_entry in backend_state.config.cache.entries:
|
|
219
|
+
logger.info(
|
|
220
|
+
f"caching {cache_entry.origin} to {cache_entry.local_path} "
|
|
221
|
+
f"and serving at {cache_entry.serve_route}"
|
|
222
|
+
)
|
|
223
|
+
route_handler = make_cache_route_handler(cache_entry, backend_state)
|
|
224
|
+
|
|
225
|
+
logger.info(
|
|
226
|
+
f"Registering route: GET {cache_entry.serve_route} with dynamic params"
|
|
227
|
+
)
|
|
228
|
+
app.add_api_route(
|
|
229
|
+
cache_entry.serve_route,
|
|
230
|
+
route_handler,
|
|
231
|
+
methods=["GET"],
|
|
232
|
+
name=f"cache-{cache_entry.serve_route.replace('/', '_')}",
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
# Frontend mount - this is a catch-all and must come LAST
|
|
236
|
+
if (
|
|
237
|
+
frontend_path := resolve_frontend(
|
|
238
|
+
path="dist-frontend", dist_name="tangram_core"
|
|
239
|
+
)
|
|
240
|
+
) is None:
|
|
241
|
+
raise ValueError(
|
|
242
|
+
"error: frontend was not found, did you run `pnpm i && pnpm run build`?"
|
|
243
|
+
)
|
|
244
|
+
app.mount("/", StaticFiles(directory=str(frontend_path), html=True), name="core")
|
|
245
|
+
|
|
246
|
+
return app
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
LOG_LEVEL_MAP = {
|
|
250
|
+
"TRACE": logging.DEBUG,
|
|
251
|
+
"DEBUG": logging.DEBUG,
|
|
252
|
+
"INFO": logging.INFO,
|
|
253
|
+
"WARN": logging.WARNING,
|
|
254
|
+
"ERROR": logging.ERROR,
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
async def run_channel_service(config: Config) -> None:
|
|
259
|
+
from . import _core
|
|
260
|
+
|
|
261
|
+
_core.init_tracing_stderr(config.core.log_level)
|
|
262
|
+
|
|
263
|
+
rust_config = _core.ChannelConfig(
|
|
264
|
+
host=config.channel.host,
|
|
265
|
+
port=config.channel.port,
|
|
266
|
+
redis_url=config.core.redis_url,
|
|
267
|
+
jwt_secret=config.channel.jwt_secret,
|
|
268
|
+
jwt_expiration_secs=config.channel.jwt_expiration_secs,
|
|
269
|
+
id_length=config.channel.id_length,
|
|
270
|
+
)
|
|
271
|
+
await _core.run(rust_config)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
async def run_services(
|
|
275
|
+
backend_state: BackendState,
|
|
276
|
+
loaded_plugins: Iterable[tuple[DistName, Plugin]],
|
|
277
|
+
) -> AsyncGenerator[asyncio.Task[None], None]:
|
|
278
|
+
yield asyncio.create_task(run_channel_service(backend_state.config))
|
|
279
|
+
|
|
280
|
+
for dist_name, plugin in loaded_plugins:
|
|
281
|
+
for _, service_func in sorted(
|
|
282
|
+
plugin.services, key=lambda s: (s[0], s[1].__name__)
|
|
283
|
+
):
|
|
284
|
+
yield asyncio.create_task(service_func(backend_state))
|
|
285
|
+
logger.info(f"started service from plugin: {dist_name}")
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
async def run_server(
|
|
289
|
+
backend_state: BackendState, loaded_plugins: list[tuple[DistName, Plugin]]
|
|
290
|
+
) -> None:
|
|
291
|
+
app_instance = create_app(backend_state, loaded_plugins)
|
|
292
|
+
server_config = uvicorn.Config(
|
|
293
|
+
app_instance,
|
|
294
|
+
host=backend_state.config.server.host,
|
|
295
|
+
port=backend_state.config.server.port,
|
|
296
|
+
log_config=get_log_config_dict(backend_state.config),
|
|
297
|
+
)
|
|
298
|
+
server = uvicorn.Server(server_config)
|
|
299
|
+
await server.serve()
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
async def start_tasks(config: Config) -> None:
|
|
303
|
+
loaded_plugins = load_enabled_plugins(config)
|
|
304
|
+
|
|
305
|
+
async with AsyncExitStack() as stack:
|
|
306
|
+
redis_client = await stack.enter_async_context(
|
|
307
|
+
redis.from_url(config.core.redis_url) # type: ignore
|
|
308
|
+
)
|
|
309
|
+
http_client = await stack.enter_async_context(httpx.AsyncClient(http2=True))
|
|
310
|
+
state = BackendState(
|
|
311
|
+
redis_client=redis_client, http_client=http_client, config=config
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
server_task = asyncio.create_task(run_server(state, loaded_plugins))
|
|
315
|
+
service_tasks = [s async for s in run_services(state, loaded_plugins)]
|
|
316
|
+
|
|
317
|
+
await asyncio.gather(server_task, *service_tasks)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def get_log_config_dict(config: Config) -> dict[str, Any]:
|
|
321
|
+
def format_time(dt: datetime) -> str:
|
|
322
|
+
return dt.astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%fZ ")
|
|
323
|
+
|
|
324
|
+
return {
|
|
325
|
+
"version": 1,
|
|
326
|
+
"disable_existing_loggers": False,
|
|
327
|
+
"handlers": {
|
|
328
|
+
"default": {
|
|
329
|
+
"class": "rich.logging.RichHandler",
|
|
330
|
+
"log_time_format": format_time,
|
|
331
|
+
"omit_repeated_times": False,
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
"root": {"handlers": ["default"], "level": config.core.log_level.upper()},
|
|
335
|
+
}
|
tangram_core/colour.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// adapted from: https://github.com/color-js/color.js/blob/main/src/spaces/oklch.js
|
|
2
|
+
type Vector3 = [number, number, number];
|
|
3
|
+
|
|
4
|
+
const multiplyMatrices = (A: number[], B: Vector3): Vector3 => {
|
|
5
|
+
return [
|
|
6
|
+
A[0] * B[0] + A[1] * B[1] + A[2] * B[2],
|
|
7
|
+
A[3] * B[0] + A[4] * B[1] + A[5] * B[2],
|
|
8
|
+
A[6] * B[0] + A[7] * B[1] + A[8] * B[2]
|
|
9
|
+
];
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const oklch2oklab = ([l, c, h]: Vector3): Vector3 => [
|
|
13
|
+
l,
|
|
14
|
+
isNaN(h) ? 0 : c * Math.cos((h * Math.PI) / 180),
|
|
15
|
+
isNaN(h) ? 0 : c * Math.sin((h * Math.PI) / 180)
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const srgbLinear2rgb = (rgb: Vector3): Vector3 =>
|
|
19
|
+
rgb.map(c =>
|
|
20
|
+
Math.abs(c) > 0.0031308
|
|
21
|
+
? (c < 0 ? -1 : 1) * (1.055 * Math.pow(Math.abs(c), 1 / 2.4) - 0.055)
|
|
22
|
+
: 12.92 * c
|
|
23
|
+
) as Vector3;
|
|
24
|
+
|
|
25
|
+
const oklab2xyz = (lab: Vector3): Vector3 => {
|
|
26
|
+
const LMSg = multiplyMatrices(
|
|
27
|
+
[
|
|
28
|
+
1, 0.3963377773761749, 0.2158037573099136, 1, -0.1055613458156586,
|
|
29
|
+
-0.0638541728258133, 1, -0.0894841775298119, -1.2914855480194092
|
|
30
|
+
],
|
|
31
|
+
lab
|
|
32
|
+
);
|
|
33
|
+
const LMS = LMSg.map(val => val ** 3) as Vector3;
|
|
34
|
+
return multiplyMatrices(
|
|
35
|
+
[
|
|
36
|
+
1.2268798758459243, -0.5578149944602171, 0.2813910456659647, -0.0405757452148008,
|
|
37
|
+
1.112286803280317, -0.0717110580655164, -0.0763729366746601, -0.4214933324022432,
|
|
38
|
+
1.5869240198367816
|
|
39
|
+
],
|
|
40
|
+
LMS
|
|
41
|
+
);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const xyz2rgbLinear = (xyz: Vector3): Vector3 => {
|
|
45
|
+
return multiplyMatrices(
|
|
46
|
+
[
|
|
47
|
+
3.2409699419045226, -1.537383177570094, -0.4986107602930034, -0.9692436362808796,
|
|
48
|
+
1.8759675015077202, 0.04155505740717559, 0.05563007969699366,
|
|
49
|
+
-0.20397695888897652, 1.0569715142428786
|
|
50
|
+
],
|
|
51
|
+
xyz
|
|
52
|
+
);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const oklch2rgb = (lch: Vector3): Vector3 =>
|
|
56
|
+
srgbLinear2rgb(xyz2rgbLinear(oklab2xyz(oklch2oklab(lch))));
|
|
57
|
+
|
|
58
|
+
export function oklchToDeckGLColor(
|
|
59
|
+
l: number,
|
|
60
|
+
c: number,
|
|
61
|
+
h: number,
|
|
62
|
+
a: number = 255
|
|
63
|
+
): [number, number, number, number] {
|
|
64
|
+
const rgb = oklch2rgb([l, c, h]);
|
|
65
|
+
return [
|
|
66
|
+
Math.max(0, Math.min(255, Math.round(rgb[0] * 255))),
|
|
67
|
+
Math.max(0, Math.min(255, Math.round(rgb[1] * 255))),
|
|
68
|
+
Math.max(0, Math.min(255, Math.round(rgb[2] * 255))),
|
|
69
|
+
a
|
|
70
|
+
];
|
|
71
|
+
}
|
tangram_core/config.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Literal
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class ServerConfig:
|
|
11
|
+
host: str = "127.0.0.1"
|
|
12
|
+
port: int = 2346
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class ChannelConfig:
|
|
17
|
+
# TODO: we should make it clear that host:port is for the *backend* to
|
|
18
|
+
# listen on, and not to be confused with the frontend.
|
|
19
|
+
host: str = "127.0.0.1"
|
|
20
|
+
port: int = 2347
|
|
21
|
+
public_url: str | None = None
|
|
22
|
+
jwt_secret: str = "secret"
|
|
23
|
+
jwt_expiration_secs: int = 315360000 # 10 years
|
|
24
|
+
id_length: int = 8
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class UrlConfig:
|
|
29
|
+
url: str
|
|
30
|
+
type: str = "vector"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class SourceSpecification:
|
|
35
|
+
carto: UrlConfig | None = None
|
|
36
|
+
protomaps: UrlConfig | None = None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class StyleSpecification:
|
|
41
|
+
sources: SourceSpecification | None = None
|
|
42
|
+
glyphs: str = "https://cdn.protomaps.com/fonts/pbf/{fontstack}/{range}.pbf"
|
|
43
|
+
layers: list[Any] | None = None
|
|
44
|
+
version: Literal[8] = 8
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class MapConfig:
|
|
49
|
+
style: str | StyleSpecification = (
|
|
50
|
+
"https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json"
|
|
51
|
+
)
|
|
52
|
+
attribution: str = (
|
|
53
|
+
'© <a href="https://www.openstreetmap.org/copyright">'
|
|
54
|
+
"OpenStreetMap</a> contributors © "
|
|
55
|
+
'<a href="https://carto.com/attributions">CARTO</a>'
|
|
56
|
+
)
|
|
57
|
+
center_lat: float = 48.0
|
|
58
|
+
center_lon: float = 7.0
|
|
59
|
+
zoom: int = 4
|
|
60
|
+
pitch: float = 0
|
|
61
|
+
bearing: float = 0
|
|
62
|
+
lang: str = "en"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class CoreConfig:
|
|
67
|
+
redis_url: str = "redis://127.0.0.1:6379"
|
|
68
|
+
plugins: list[str] = field(default_factory=list)
|
|
69
|
+
log_level: str = "INFO"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass
|
|
73
|
+
class CacheEntry:
|
|
74
|
+
# origin url (if None, just serve the local file)
|
|
75
|
+
origin: str | None = None
|
|
76
|
+
# local path to cache the file
|
|
77
|
+
local_path: Path | None = None
|
|
78
|
+
# how to serve the file
|
|
79
|
+
serve_route: str = ""
|
|
80
|
+
# media type for the served file
|
|
81
|
+
media_type: str = "application/octet-stream"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass
|
|
85
|
+
class CacheConfig:
|
|
86
|
+
entries: list[CacheEntry] = field(default_factory=list)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass
|
|
90
|
+
class Config:
|
|
91
|
+
core: CoreConfig = field(default_factory=CoreConfig)
|
|
92
|
+
server: ServerConfig = field(default_factory=ServerConfig)
|
|
93
|
+
channel: ChannelConfig = field(default_factory=ChannelConfig)
|
|
94
|
+
map: MapConfig = field(default_factory=MapConfig)
|
|
95
|
+
plugins: dict[str, Any] = field(default_factory=dict)
|
|
96
|
+
cache: CacheConfig = field(default_factory=CacheConfig)
|
|
97
|
+
|
|
98
|
+
@classmethod
|
|
99
|
+
def from_file(cls, config_path: Path) -> Config:
|
|
100
|
+
if sys.version_info < (3, 11):
|
|
101
|
+
import tomli as tomllib
|
|
102
|
+
else:
|
|
103
|
+
import tomllib
|
|
104
|
+
from pydantic import TypeAdapter
|
|
105
|
+
|
|
106
|
+
with open(config_path, "rb") as f:
|
|
107
|
+
cfg_data = tomllib.load(f)
|
|
108
|
+
|
|
109
|
+
config_adapter = TypeAdapter(cls)
|
|
110
|
+
config = config_adapter.validate_python(cfg_data)
|
|
111
|
+
return config
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@dataclass
|
|
115
|
+
class FrontendChannelConfig:
|
|
116
|
+
url: str
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@dataclass
|
|
120
|
+
class FrontendConfig:
|
|
121
|
+
channel: FrontendChannelConfig
|
|
122
|
+
map: MapConfig
|