goodmap 2.0.0a1__tar.gz → 2.0.0a2__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.
Files changed (25) hide show
  1. {goodmap-2.0.0a1 → goodmap-2.0.0a2}/PKG-INFO +2 -2
  2. goodmap-2.0.0a2/goodmap/__init__.py +3 -0
  3. {goodmap-2.0.0a1 → goodmap-2.0.0a2}/goodmap/goodmap.py +76 -19
  4. goodmap-2.0.0a2/goodmap/plugin.py +75 -0
  5. goodmap-2.0.0a2/goodmap/static/frontend/index.min.js +246 -0
  6. {goodmap-2.0.0a1 → goodmap-2.0.0a2}/pyproject.toml +2 -2
  7. goodmap-2.0.0a1/goodmap/__init__.py +0 -3
  8. goodmap-2.0.0a1/goodmap/plugin.py +0 -12
  9. goodmap-2.0.0a1/goodmap/static/frontend/index.min.js +0 -246
  10. {goodmap-2.0.0a1 → goodmap-2.0.0a2}/LICENSE.md +0 -0
  11. {goodmap-2.0.0a1 → goodmap-2.0.0a2}/README.md +0 -0
  12. {goodmap-2.0.0a1 → goodmap-2.0.0a2}/goodmap/admin_api.py +0 -0
  13. {goodmap-2.0.0a1 → goodmap-2.0.0a2}/goodmap/api_models.py +0 -0
  14. {goodmap-2.0.0a1 → goodmap-2.0.0a2}/goodmap/clustering.py +0 -0
  15. {goodmap-2.0.0a1 → goodmap-2.0.0a2}/goodmap/config.py +0 -0
  16. {goodmap-2.0.0a1 → goodmap-2.0.0a2}/goodmap/core.py +0 -0
  17. {goodmap-2.0.0a1 → goodmap-2.0.0a2}/goodmap/core_api.py +0 -0
  18. {goodmap-2.0.0a1 → goodmap-2.0.0a2}/goodmap/data_models/location.py +0 -0
  19. {goodmap-2.0.0a1 → goodmap-2.0.0a2}/goodmap/db.py +0 -0
  20. {goodmap-2.0.0a1 → goodmap-2.0.0a2}/goodmap/exceptions.py +0 -0
  21. {goodmap-2.0.0a1 → goodmap-2.0.0a2}/goodmap/feature_flags.py +0 -0
  22. {goodmap-2.0.0a1 → goodmap-2.0.0a2}/goodmap/formatter.py +0 -0
  23. {goodmap-2.0.0a1 → goodmap-2.0.0a2}/goodmap/json_security.py +0 -0
  24. {goodmap-2.0.0a1 → goodmap-2.0.0a2}/goodmap/templates/goodmap-admin.html +0 -0
  25. {goodmap-2.0.0a1 → goodmap-2.0.0a2}/goodmap/templates/map.html +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: goodmap
3
- Version: 2.0.0a1
3
+ Version: 2.0.0a2
4
4
  Summary: Map engine to serve all the people :)
5
5
  Author: Krzysztof Kolodzinski
6
6
  Author-email: krzysztof.kolodzinski@problematy.pl
@@ -23,7 +23,7 @@ Requires-Dist: gunicorn (>=20.1,<26.0)
23
23
  Requires-Dist: humanize (>=4.6.0,<5.0.0)
24
24
  Requires-Dist: myst-parser (>=4.0.0,<5.0.0) ; extra == "docs"
25
25
  Requires-Dist: numpy (>=2.2.0,<3.0.0)
26
- Requires-Dist: platzky (==2.0.0a8)
26
+ Requires-Dist: platzky (==2.0.0a10)
27
27
  Requires-Dist: pydantic (>=2.12.0,<3.0.0)
28
28
  Requires-Dist: pysupercluster-problematy (>=0.7.8,<0.8.0)
29
29
  Requires-Dist: scipy (>=1.15.1,<2.0.0)
@@ -0,0 +1,3 @@
1
+ from goodmap.plugin import MapOverlayPluginBase, MarkerFieldPluginBase
2
+
3
+ __all__ = ["MapOverlayPluginBase", "MarkerFieldPluginBase"]
@@ -25,32 +25,55 @@ from goodmap.db import (
25
25
  get_location_obligatory_fields,
26
26
  )
27
27
  from goodmap.feature_flags import EnableAdminPanel, UseLazyLoading
28
+ from goodmap.plugin import CAPABILITY_BASES, GoodmapPluginBase
28
29
 
29
30
  logger = logging.getLogger(__name__)
30
31
 
31
32
  _PLUGIN_ENTRY_POINT_GROUP = "goodmap.plugins"
32
33
 
33
34
 
35
+ def _frontend_capability_bases(plugin_class: Any) -> list[type[GoodmapPluginBase]]:
36
+ """The goodmap frontend capability bases a plugin subclasses (may be several).
37
+
38
+ Derived by class-based recognition, the same way platzky matches plugins against its
39
+ capability bases. A plugin can provide multiple frontend capabilities (e.g. an overlay
40
+ and a marker field) by subclassing more than one base.
41
+ """
42
+ if not isinstance(plugin_class, type):
43
+ return []
44
+ return [base for base in CAPABILITY_BASES if issubclass(plugin_class, base)]
45
+
46
+
34
47
  def _register_plugin_static_resources(
35
48
  ep: importlib.metadata.EntryPoint,
36
- ) -> tuple[Blueprint | None, dict[str, Any] | None]:
37
- """Load a plugin's static resources and return its blueprint and manifest entry.
49
+ ) -> tuple[Blueprint | None, list[dict[str, Any]]]:
50
+ """Load a plugin's static resources and return its blueprint and manifest entries.
38
51
 
39
- Loads the plugin module, checks for a 'static' directory, and if found
40
- creates a Flask blueprint and a manifest entry for the frontend.
52
+ Loads the plugin module, checks for a 'static' directory, and if found creates a Flask
53
+ blueprint plus one manifest entry per frontend capability the plugin provides. Each
54
+ capability points at its own Module Federation ``module``, all served from the plugin's
55
+ single ``remoteEntry.js``.
41
56
 
42
57
  Args:
43
58
  ep: The entry point for the plugin.
44
59
 
45
60
  Returns:
46
- A tuple of (blueprint, manifest_entry). Both are None if the plugin
47
- has no static directory or loading fails.
61
+ A tuple of (blueprint, manifest_entries). The blueprint is None and the list empty
62
+ if the plugin has no static directory, no frontend capability, or loading fails.
48
63
  """
49
64
  try:
50
- mod_path = os.path.dirname(os.path.realpath(inspect.getfile(ep.load())))
65
+ plugin_class = ep.load()
66
+ mod_path = os.path.dirname(os.path.realpath(inspect.getfile(plugin_class)))
51
67
  static_dir = os.path.join(mod_path, "static")
52
68
  if not os.path.isdir(static_dir):
53
- return None, None
69
+ return None, []
70
+
71
+ # One manifest entry per capability the plugin provides. "capability" tells the
72
+ # frontend which handler mounts the component (overlay via MapOverlays, field via
73
+ # FieldRenderer, …) and "module" is that capability's Module Federation key.
74
+ bases = _frontend_capability_bases(plugin_class)
75
+ if not bases:
76
+ return None, []
54
77
 
55
78
  bp = Blueprint(
56
79
  f"plugin_{ep.name}",
@@ -65,15 +88,24 @@ def _register_plugin_static_resources(
65
88
  response.headers["Access-Control-Allow-Origin"] = "*"
66
89
  return response
67
90
 
68
- manifest_entry = {
69
- "scope": ep.name,
70
- "url": f"/plugins/{ep.name}/static/remoteEntry.js",
71
- "module": "./Button",
72
- }
73
- return bp, manifest_entry
91
+ entries = []
92
+ for base in bases:
93
+ # The capability token and its Module Federation key are derived from the base
94
+ # class name (MapOverlayPluginBase -> "MapOverlay" / "./MapOverlay"), so the
95
+ # class is the single source of truth — no separate identifier to keep in sync.
96
+ capability = base.__name__.removesuffix("PluginBase")
97
+ entries.append(
98
+ {
99
+ "pluginName": ep.name,
100
+ "url": f"/plugins/{ep.name}/static/remoteEntry.js",
101
+ "module": f"./{capability}",
102
+ "capability": capability,
103
+ }
104
+ )
105
+ return bp, entries
74
106
  except Exception:
75
107
  logger.warning("Failed to serve static files for plugin '%s'", ep.name)
76
- return None, None
108
+ return None, []
77
109
 
78
110
 
79
111
  def _setup_location_model(
@@ -133,7 +165,15 @@ def create_app_from_config(config: GoodmapConfig) -> platzky.Engine:
133
165
 
134
166
  locale_dir = os.path.join(directory, "locale")
135
167
  config.translation_directories.append(locale_dir)
136
- app = platzky.create_app_from_config(config)
168
+ # Register goodmap's own plugin ecosystem with platzky: MapOverlayPluginBase is a
169
+ # host-defined capability, and goodmap plugins are discovered from the
170
+ # "goodmap.plugins" entry-point group. This makes them config-gated (is_active)
171
+ # through platzky's normal plugin loader, alongside platzky's own plugins.
172
+ app = platzky.create_app_from_config(
173
+ config,
174
+ extra_plugin_bases=list(CAPABILITY_BASES),
175
+ extra_plugins_entrypoints=[_PLUGIN_ENTRY_POINT_GROUP],
176
+ )
137
177
 
138
178
  frontend_static_dir = os.path.join(directory, "static", "frontend")
139
179
  app.register_blueprint(
@@ -160,12 +200,29 @@ def create_app_from_config(config: GoodmapConfig) -> platzky.Engine:
160
200
 
161
201
  app.extensions["goodmap"] = {"location_obligatory_fields": location_obligatory_fields}
162
202
 
203
+ try:
204
+ plugins_data = app.db.get_plugins_data()
205
+ except Exception:
206
+ logger.warning(
207
+ "Could not read plugin config data; frontend plugins get empty config",
208
+ exc_info=True,
209
+ )
210
+ plugins_data = {}
211
+
163
212
  plugin_manifest = []
164
213
  for ep in importlib.metadata.entry_points(group=_PLUGIN_ENTRY_POINT_GROUP):
165
- bp, entry = _register_plugin_static_resources(ep)
166
- if bp is not None:
214
+ plugin_cfg = plugins_data.get(ep.name)
215
+ # Only serve the frontend for plugins that are explicitly enabled in config.
216
+ # platzky's loader has already instantiated the active ones (gated identically),
217
+ # so the manifest stays in lockstep with the loaded backend plugins.
218
+ if plugin_cfg is None or not plugin_cfg.is_active:
219
+ continue
220
+ bp, entries = _register_plugin_static_resources(ep)
221
+ if bp is not None and entries:
167
222
  app.register_blueprint(bp)
168
- plugin_manifest.append(entry)
223
+ for entry in entries:
224
+ entry["config"] = plugin_cfg.config
225
+ plugin_manifest.append(entry)
169
226
 
170
227
  app.config["PLUGIN_MANIFEST"] = plugin_manifest
171
228
 
@@ -0,0 +1,75 @@
1
+ """Base class for goodmap map plugins."""
2
+
3
+ from typing import Any
4
+
5
+ from platzky.plugin.plugin import PluginBase
6
+
7
+
8
+ class GoodmapPluginBase(PluginBase):
9
+ """Base class (family root) for goodmap plugin capabilities.
10
+
11
+ A goodmap plugin declares its frontend capabilities by subclassing the concrete
12
+ capability bases below (one or more). goodmap derives each capability's manifest
13
+ token and Module Federation module from the base class name — ``MapOverlayPluginBase``
14
+ -> capability ``"MapOverlay"`` exposed as ``"./MapOverlay"`` — so the class is the
15
+ single source of truth and there is no separate identifier to keep in sync.
16
+ """
17
+
18
+ def __init__(self, config: dict[str, Any]) -> None:
19
+ super().__init__(config)
20
+
21
+
22
+ class MapOverlayPluginBase(GoodmapPluginBase):
23
+ """Capability: a plugin that renders an overlay on top of the map view.
24
+
25
+ Map-overlay plugins contribute a frontend component (served via Module
26
+ Federation) that is mounted globally over the map — e.g. a banner shown when
27
+ no points are visible. They do not transform point/location data; for that,
28
+ use platzky's ``ContentTransformerPluginBase`` instead.
29
+
30
+ goodmap registers this capability with platzky (via ``extra_plugin_bases``)
31
+ so overlay plugins are config-gated through the standard plugin loader.
32
+
33
+ Manifest capability ``"MapOverlay"``; component exposed as ``"./MapOverlay"``.
34
+ """
35
+
36
+ def __init__(self, config: dict[str, Any]) -> None:
37
+ super().__init__(config)
38
+
39
+
40
+ class MarkerFieldPluginBase(GoodmapPluginBase):
41
+ """Capability: a plugin that renders (or wraps) a marker-popup field.
42
+
43
+ Every field plugin is the same kind of thing — a *wrapper* in the field's rendering
44
+ fold, mounted by ``FieldRenderer``. Its ``config`` declares:
45
+
46
+ - ``field``: the field ``type`` it attaches to (e.g. ``"hyperlink"``, or a custom type
47
+ whose value the plugin's platzky shortcode produces as ``{"type": "<field>", ...}``).
48
+ - ``order`` (optional): its position in the stack — lower is more innermost; ties keep
49
+ registration order.
50
+
51
+ ``FieldRenderer`` pipes the field's raw value through a chain of stages: the built-in for
52
+ the type (if any) renders it, then each plugin for that ``field`` transforms the result,
53
+ innermost-first. Each stage is ``({ input, config }) => element`` and receives the previous
54
+ stage's output as ``input`` — so the innermost stage gets the raw value and renders from
55
+ it, and every later stage gets the current element and wraps it. (A wrapping plugin thus
56
+ requires something to render the type: a built-in, or a renderer it ships with or depends
57
+ on; a type with only wrappers and no renderer is a misconfiguration.)
58
+
59
+ goodmap registers this capability with platzky (via ``extra_plugin_bases``) so field
60
+ plugins are config-gated through the standard plugin loader.
61
+
62
+ Manifest capability ``"MarkerField"``; component exposed as ``"./MarkerField"``.
63
+ """
64
+
65
+ def __init__(self, config: dict[str, Any]) -> None:
66
+ super().__init__(config)
67
+
68
+
69
+ # The frontend plugin capabilities goodmap defines: registered with platzky (so plugins
70
+ # subclassing them are recognised and config-gated) and used to derive each plugin's
71
+ # manifest entries. A plugin may subclass one or more of these.
72
+ CAPABILITY_BASES: tuple[type[GoodmapPluginBase], ...] = (
73
+ MapOverlayPluginBase,
74
+ MarkerFieldPluginBase,
75
+ )