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.
Files changed (171) hide show
  1. tangram_core/App.vue +335 -0
  2. tangram_core/__init__.py +5 -0
  3. tangram_core/__main__.py +141 -0
  4. tangram_core/_core.cp313-win_amd64.pyd +0 -0
  5. tangram_core/_core.pyi +38 -0
  6. tangram_core/api.ts +456 -0
  7. tangram_core/backend.py +335 -0
  8. tangram_core/colour.ts +71 -0
  9. tangram_core/config.py +122 -0
  10. tangram_core/dist-frontend/aggregation-layers.js +521 -0
  11. tangram_core/dist-frontend/aggregation-layers.js.map +1 -0
  12. tangram_core/dist-frontend/assets/_commonjsHelpers-CqkleIqs.js +2 -0
  13. tangram_core/dist-frontend/assets/_commonjsHelpers-CqkleIqs.js.map +1 -0
  14. tangram_core/dist-frontend/assets/array-utils-flat-wyE8tIYR.js +11 -0
  15. tangram_core/dist-frontend/assets/array-utils-flat-wyE8tIYR.js.map +1 -0
  16. tangram_core/dist-frontend/assets/assert-hrfsarFU.js +3 -0
  17. tangram_core/dist-frontend/assets/assert-hrfsarFU.js.map +1 -0
  18. tangram_core/dist-frontend/assets/b612-latin-400-italic-DePNXA0a.woff +0 -0
  19. tangram_core/dist-frontend/assets/b612-latin-400-italic-a-4GLPtl.woff2 +0 -0
  20. tangram_core/dist-frontend/assets/b612-latin-400-normal-CC98FVm_.woff2 +0 -0
  21. tangram_core/dist-frontend/assets/b612-latin-400-normal-JbZ7xwUX.woff +0 -0
  22. tangram_core/dist-frontend/assets/b612-latin-700-normal-B_Snq1wd.woff +0 -0
  23. tangram_core/dist-frontend/assets/b612-latin-700-normal-BinQrnoB.woff2 +0 -0
  24. tangram_core/dist-frontend/assets/clip-extension-DTCP51Ak.js +26 -0
  25. tangram_core/dist-frontend/assets/clip-extension-DTCP51Ak.js.map +1 -0
  26. tangram_core/dist-frontend/assets/color-CUNNsFV-.js +17 -0
  27. tangram_core/dist-frontend/assets/color-CUNNsFV-.js.map +1 -0
  28. tangram_core/dist-frontend/assets/cube-geometry-CzJ_uBWa.js +2 -0
  29. tangram_core/dist-frontend/assets/cube-geometry-CzJ_uBWa.js.map +1 -0
  30. tangram_core/dist-frontend/assets/deep-equal-uriyKJca.js +2 -0
  31. tangram_core/dist-frontend/assets/deep-equal-uriyKJca.js.map +1 -0
  32. tangram_core/dist-frontend/assets/fly-to-interpolator-DlKiy9_S.js +2 -0
  33. tangram_core/dist-frontend/assets/fly-to-interpolator-DlKiy9_S.js.map +1 -0
  34. tangram_core/dist-frontend/assets/geojson-layer-CLhXLxdI.js +1010 -0
  35. tangram_core/dist-frontend/assets/geojson-layer-CLhXLxdI.js.map +1 -0
  36. tangram_core/dist-frontend/assets/globe-view-DKhftlA1.js +94 -0
  37. tangram_core/dist-frontend/assets/globe-view-DKhftlA1.js.map +1 -0
  38. tangram_core/dist-frontend/assets/globe-viewport-CPES4D4P.js +2 -0
  39. tangram_core/dist-frontend/assets/globe-viewport-CPES4D4P.js.map +1 -0
  40. tangram_core/dist-frontend/assets/image-loader-ClbNCMXW.js +2 -0
  41. tangram_core/dist-frontend/assets/image-loader-ClbNCMXW.js.map +1 -0
  42. tangram_core/dist-frontend/assets/inconsolata-latin-400-normal-DTZQ6lD6.woff2 +0 -0
  43. tangram_core/dist-frontend/assets/inconsolata-latin-400-normal-HYADljCo.woff +0 -0
  44. tangram_core/dist-frontend/assets/inconsolata-latin-700-normal-ByjKuJjN.woff2 +0 -0
  45. tangram_core/dist-frontend/assets/inconsolata-latin-700-normal-DzgUY3Rl.woff +0 -0
  46. tangram_core/dist-frontend/assets/inconsolata-latin-ext-400-normal-BaHVOdFB.woff2 +0 -0
  47. tangram_core/dist-frontend/assets/inconsolata-latin-ext-400-normal-yvPjCxxx.woff +0 -0
  48. tangram_core/dist-frontend/assets/inconsolata-latin-ext-700-normal-D0Kpgs_9.woff2 +0 -0
  49. tangram_core/dist-frontend/assets/inconsolata-latin-ext-700-normal-Dlt-daqV.woff +0 -0
  50. tangram_core/dist-frontend/assets/inconsolata-vietnamese-400-normal-ByiM2lek.woff +0 -0
  51. tangram_core/dist-frontend/assets/inconsolata-vietnamese-400-normal-DfC_iMic.woff2 +0 -0
  52. tangram_core/dist-frontend/assets/inconsolata-vietnamese-700-normal-DLCFFAUf.woff +0 -0
  53. tangram_core/dist-frontend/assets/inconsolata-vietnamese-700-normal-DuasYmn8.woff2 +0 -0
  54. tangram_core/dist-frontend/assets/index-UPPakSLR.css +1 -0
  55. tangram_core/dist-frontend/assets/index-r8T0kY2p.js +821 -0
  56. tangram_core/dist-frontend/assets/index-r8T0kY2p.js.map +1 -0
  57. tangram_core/dist-frontend/assets/layer-DO63TrsS.js +555 -0
  58. tangram_core/dist-frontend/assets/layer-DO63TrsS.js.map +1 -0
  59. tangram_core/dist-frontend/assets/layer-extension-CZ3zsHuN.js +2 -0
  60. tangram_core/dist-frontend/assets/layer-extension-CZ3zsHuN.js.map +1 -0
  61. tangram_core/dist-frontend/assets/mesh-layers-BSECKarm.js +1123 -0
  62. tangram_core/dist-frontend/assets/mesh-layers-BSECKarm.js.map +1 -0
  63. tangram_core/dist-frontend/assets/orthographic-viewport-CzZmHDEZ.js +2 -0
  64. tangram_core/dist-frontend/assets/orthographic-viewport-CzZmHDEZ.js.map +1 -0
  65. tangram_core/dist-frontend/assets/pick-layers-pass-xhWsgZtf.js +2 -0
  66. tangram_core/dist-frontend/assets/pick-layers-pass-xhWsgZtf.js.map +1 -0
  67. tangram_core/dist-frontend/assets/project-CrvReKGW.js +760 -0
  68. tangram_core/dist-frontend/assets/project-CrvReKGW.js.map +1 -0
  69. tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-400-italic-4qS3_zkX.woff2 +0 -0
  70. tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-400-italic-CDK-EZBY.woff +0 -0
  71. tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-400-normal-Bgns473E.woff +0 -0
  72. tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-400-normal-_T2aQlWs.woff2 +0 -0
  73. tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-500-normal-CvEVpWxD.woff +0 -0
  74. tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-500-normal-s4PklZE0.woff2 +0 -0
  75. tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-700-normal-9RN-Z7cI.woff2 +0 -0
  76. tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-700-normal-BGMkBBYx.woff +0 -0
  77. tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-ext-400-italic-C7erd-g8.woff +0 -0
  78. tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-ext-400-italic-DR5R5TWx.woff2 +0 -0
  79. tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-ext-400-normal-DGo1Ayjq.woff2 +0 -0
  80. tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-ext-400-normal-WtM1l1qc.woff +0 -0
  81. tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-ext-500-normal-C8FNIdXm.woff2 +0 -0
  82. tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-ext-500-normal-TLDmfi3Q.woff +0 -0
  83. tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-ext-700-normal-CTXjXnze.woff2 +0 -0
  84. tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-ext-700-normal-CWPRiRXS.woff +0 -0
  85. tangram_core/dist-frontend/assets/roboto-condensed-greek-400-italic-CR6qj4Z4.woff2 +0 -0
  86. tangram_core/dist-frontend/assets/roboto-condensed-greek-400-italic-DHRaIs10.woff +0 -0
  87. tangram_core/dist-frontend/assets/roboto-condensed-greek-400-normal-D5vBSIyg.woff2 +0 -0
  88. tangram_core/dist-frontend/assets/roboto-condensed-greek-400-normal-FabMgVmk.woff +0 -0
  89. tangram_core/dist-frontend/assets/roboto-condensed-greek-500-normal-BIN62cw9.woff +0 -0
  90. tangram_core/dist-frontend/assets/roboto-condensed-greek-500-normal-Hsn-wDIp.woff2 +0 -0
  91. tangram_core/dist-frontend/assets/roboto-condensed-greek-700-normal-89Up2Xly.woff +0 -0
  92. tangram_core/dist-frontend/assets/roboto-condensed-greek-700-normal-DWMOA2VK.woff2 +0 -0
  93. tangram_core/dist-frontend/assets/roboto-condensed-latin-400-italic-D_BR-3LG.woff2 +0 -0
  94. tangram_core/dist-frontend/assets/roboto-condensed-latin-400-italic-om57GXsO.woff +0 -0
  95. tangram_core/dist-frontend/assets/roboto-condensed-latin-400-normal-BICmKrXV.woff2 +0 -0
  96. tangram_core/dist-frontend/assets/roboto-condensed-latin-400-normal-D2e7XwB1.woff +0 -0
  97. tangram_core/dist-frontend/assets/roboto-condensed-latin-500-normal-3p2daRJW.woff2 +0 -0
  98. tangram_core/dist-frontend/assets/roboto-condensed-latin-500-normal-Dc9bsamC.woff +0 -0
  99. tangram_core/dist-frontend/assets/roboto-condensed-latin-700-normal-BOl6B_hI.woff +0 -0
  100. tangram_core/dist-frontend/assets/roboto-condensed-latin-700-normal-DRbp0YnP.woff2 +0 -0
  101. tangram_core/dist-frontend/assets/roboto-condensed-latin-ext-400-italic-BXrkWnoY.woff +0 -0
  102. tangram_core/dist-frontend/assets/roboto-condensed-latin-ext-400-italic-Bhem1d5z.woff2 +0 -0
  103. tangram_core/dist-frontend/assets/roboto-condensed-latin-ext-400-normal-DT8nEsYA.woff +0 -0
  104. tangram_core/dist-frontend/assets/roboto-condensed-latin-ext-400-normal-OHaX69iP.woff2 +0 -0
  105. tangram_core/dist-frontend/assets/roboto-condensed-latin-ext-500-normal-CcSTXKtO.woff2 +0 -0
  106. tangram_core/dist-frontend/assets/roboto-condensed-latin-ext-500-normal-JgPl2bDS.woff +0 -0
  107. tangram_core/dist-frontend/assets/roboto-condensed-latin-ext-700-normal-B004qtqu.woff2 +0 -0
  108. tangram_core/dist-frontend/assets/roboto-condensed-latin-ext-700-normal-O6H_RRvN.woff +0 -0
  109. tangram_core/dist-frontend/assets/roboto-condensed-vietnamese-400-italic-BwUYFJ2t.woff2 +0 -0
  110. tangram_core/dist-frontend/assets/roboto-condensed-vietnamese-400-italic-DV8QogUk.woff +0 -0
  111. tangram_core/dist-frontend/assets/roboto-condensed-vietnamese-400-normal-0o1laQ-g.woff2 +0 -0
  112. tangram_core/dist-frontend/assets/roboto-condensed-vietnamese-400-normal-CPsdS8_S.woff +0 -0
  113. tangram_core/dist-frontend/assets/roboto-condensed-vietnamese-500-normal-G9shSJ2z.woff +0 -0
  114. tangram_core/dist-frontend/assets/roboto-condensed-vietnamese-500-normal-TFWhjk13.woff2 +0 -0
  115. tangram_core/dist-frontend/assets/roboto-condensed-vietnamese-700-normal-BtNeb9D6.woff +0 -0
  116. tangram_core/dist-frontend/assets/roboto-condensed-vietnamese-700-normal-D35V1G0s.woff2 +0 -0
  117. tangram_core/dist-frontend/assets/shader-BJmsOfPx.js +843 -0
  118. tangram_core/dist-frontend/assets/shader-BJmsOfPx.js.map +1 -0
  119. tangram_core/dist-frontend/assets/solid-polygon-layer-DiarVGxh.js +392 -0
  120. tangram_core/dist-frontend/assets/solid-polygon-layer-DiarVGxh.js.map +1 -0
  121. tangram_core/dist-frontend/assets/tesselator-49Dw9L5A.js +2 -0
  122. tangram_core/dist-frontend/assets/tesselator-49Dw9L5A.js.map +1 -0
  123. tangram_core/dist-frontend/assets/webgl-developer-tools-CZl8qVFg.js +7 -0
  124. tangram_core/dist-frontend/assets/webgl-developer-tools-CZl8qVFg.js.map +1 -0
  125. tangram_core/dist-frontend/assets/webgl-device-BY0-CUP6.js +3 -0
  126. tangram_core/dist-frontend/assets/webgl-device-BY0-CUP6.js.map +1 -0
  127. tangram_core/dist-frontend/assets/widget-BbOeHGj0.js +2 -0
  128. tangram_core/dist-frontend/assets/widget-BbOeHGj0.js.map +1 -0
  129. tangram_core/dist-frontend/core.js +60 -0
  130. tangram_core/dist-frontend/core.js.map +1 -0
  131. tangram_core/dist-frontend/extensions.js +609 -0
  132. tangram_core/dist-frontend/extensions.js.map +1 -0
  133. tangram_core/dist-frontend/favicon.ico +0 -0
  134. tangram_core/dist-frontend/favicon.png +0 -0
  135. tangram_core/dist-frontend/font-awesome.min.css +4 -0
  136. tangram_core/dist-frontend/fonts/FontAwesome.otf +0 -0
  137. tangram_core/dist-frontend/fonts/fontawesome-webfont.eot +0 -0
  138. tangram_core/dist-frontend/fonts/fontawesome-webfont.svg +2671 -0
  139. tangram_core/dist-frontend/fonts/fontawesome-webfont.ttf +0 -0
  140. tangram_core/dist-frontend/fonts/fontawesome-webfont.woff +0 -0
  141. tangram_core/dist-frontend/fonts/fontawesome-webfont.woff2 +0 -0
  142. tangram_core/dist-frontend/geo-layers.js +115 -0
  143. tangram_core/dist-frontend/geo-layers.js.map +1 -0
  144. tangram_core/dist-frontend/index.html +38 -0
  145. tangram_core/dist-frontend/json.js +3 -0
  146. tangram_core/dist-frontend/json.js.map +1 -0
  147. tangram_core/dist-frontend/layers.js +268 -0
  148. tangram_core/dist-frontend/layers.js.map +1 -0
  149. tangram_core/dist-frontend/lit-html.js +7 -0
  150. tangram_core/dist-frontend/lit-html.js.map +1 -0
  151. tangram_core/dist-frontend/mapbox.js +2 -0
  152. tangram_core/dist-frontend/mapbox.js.map +1 -0
  153. tangram_core/dist-frontend/maplibre-gl.js +59 -0
  154. tangram_core/dist-frontend/maplibre-gl.js.map +1 -0
  155. tangram_core/dist-frontend/mesh-layers.js +2 -0
  156. tangram_core/dist-frontend/mesh-layers.js.map +1 -0
  157. tangram_core/dist-frontend/rs1090_wasm.js +813 -0
  158. tangram_core/dist-frontend/rs1090_wasm_bg.wasm +0 -0
  159. tangram_core/dist-frontend/vue.esm-browser.prod.js +13 -0
  160. tangram_core/dist-frontend/widgets.js +3 -0
  161. tangram_core/dist-frontend/widgets.js.map +1 -0
  162. tangram_core/main.ts +16 -0
  163. tangram_core/plugin.py +70 -0
  164. tangram_core/plugin.ts +41 -0
  165. tangram_core/redis.py +89 -0
  166. tangram_core/user.css +114 -0
  167. tangram_core/vite-plugin-tangram.mjs +88 -0
  168. tangram_core-0.2.0.dist-info/METADATA +37 -0
  169. tangram_core-0.2.0.dist-info/RECORD +171 -0
  170. tangram_core-0.2.0.dist-info/WHEEL +4 -0
  171. tangram_core-0.2.0.dist-info/entry_points.txt +2 -0
@@ -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
+ '&copy; <a href="https://www.openstreetmap.org/copyright">'
54
+ "OpenStreetMap</a> contributors &copy; "
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