dasf-exb-reference 0.1.0__tar.gz → 0.2.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.
Files changed (28) hide show
  1. {dasf_exb_reference-0.1.0/dasf_exb_reference.egg-info → dasf_exb_reference-0.2.0}/PKG-INFO +9 -5
  2. {dasf_exb_reference-0.1.0 → dasf_exb_reference-0.2.0}/dasf_exb/_version.py +3 -3
  3. {dasf_exb_reference-0.1.0 → dasf_exb_reference-0.2.0}/dasf_exb/backend.py +34 -5
  4. {dasf_exb_reference-0.1.0 → dasf_exb_reference-0.2.0}/dasf_exb/download_geodatasets.py +1 -1
  5. dasf_exb_reference-0.2.0/dasf_exb/input_module.py +177 -0
  6. {dasf_exb_reference-0.1.0 → dasf_exb_reference-0.2.0}/dasf_exb/return_models.py +3 -3
  7. {dasf_exb_reference-0.1.0 → dasf_exb_reference-0.2.0/dasf_exb_reference.egg-info}/PKG-INFO +9 -5
  8. {dasf_exb_reference-0.1.0 → dasf_exb_reference-0.2.0}/dasf_exb_reference.egg-info/SOURCES.txt +4 -1
  9. {dasf_exb_reference-0.1.0 → dasf_exb_reference-0.2.0}/dasf_exb_reference.egg-info/requires.txt +10 -4
  10. {dasf_exb_reference-0.1.0 → dasf_exb_reference-0.2.0}/pyproject.toml +14 -5
  11. dasf_exb_reference-0.2.0/tests/test_backend.py +123 -0
  12. dasf_exb_reference-0.2.0/tests/test_connected_module.py +41 -0
  13. dasf_exb_reference-0.2.0/tests/test_input_module.py +226 -0
  14. dasf_exb_reference-0.1.0/tests/test_backend.py +0 -58
  15. {dasf_exb_reference-0.1.0 → dasf_exb_reference-0.2.0}/LICENSES/Apache-2.0.txt +0 -0
  16. {dasf_exb_reference-0.1.0 → dasf_exb_reference-0.2.0}/LICENSES/CC-BY-4.0.txt +0 -0
  17. {dasf_exb_reference-0.1.0 → dasf_exb_reference-0.2.0}/LICENSES/CC0-1.0.txt +0 -0
  18. {dasf_exb_reference-0.1.0 → dasf_exb_reference-0.2.0}/MANIFEST.in +0 -0
  19. {dasf_exb_reference-0.1.0 → dasf_exb_reference-0.2.0}/README.md +0 -0
  20. {dasf_exb_reference-0.1.0 → dasf_exb_reference-0.2.0}/dasf_exb/__init__.py +0 -0
  21. {dasf_exb_reference-0.1.0 → dasf_exb_reference-0.2.0}/dasf_exb/input_models.py +0 -0
  22. {dasf_exb_reference-0.1.0 → dasf_exb_reference-0.2.0}/dasf_exb/py.typed +0 -0
  23. {dasf_exb_reference-0.1.0 → dasf_exb_reference-0.2.0}/dasf_exb_reference.egg-info/dependency_links.txt +0 -0
  24. {dasf_exb_reference-0.1.0 → dasf_exb_reference-0.2.0}/dasf_exb_reference.egg-info/not-zip-safe +0 -0
  25. {dasf_exb_reference-0.1.0 → dasf_exb_reference-0.2.0}/dasf_exb_reference.egg-info/top_level.txt +0 -0
  26. {dasf_exb_reference-0.1.0 → dasf_exb_reference-0.2.0}/setup.cfg +0 -0
  27. {dasf_exb_reference-0.1.0 → dasf_exb_reference-0.2.0}/setup.py +0 -0
  28. {dasf_exb_reference-0.1.0 → dasf_exb_reference-0.2.0}/tests/test_imports.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dasf-exb-reference
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Reference implementation and Demonstration of a DASF Backend Module visualized by the dasf-experiencebuilder-plugin
5
5
  Author-email: "Philipp S. Sommer" <philipp.sommer@hereon.de>, Markus Benninghoff <markus.benninghoff@hereon.de>
6
6
  Maintainer-email: "Philipp S. Sommer" <philipp.sommer@hereon.de>, Markus Benninghoff <markus.benninghoff@hereon.de>
@@ -26,13 +26,17 @@ Description-Content-Type: text/markdown
26
26
  License-File: LICENSES/Apache-2.0.txt
27
27
  License-File: LICENSES/CC-BY-4.0.txt
28
28
  License-File: LICENSES/CC0-1.0.txt
29
- Requires-Dist: demessaging>=0.4.0
29
+ Requires-Dist: dasf>=1.0.1
30
30
  Provides-Extra: backend
31
- Requires-Dist: demessaging[backend]; extra == "backend"
31
+ Requires-Dist: dasf[backend]; extra == "backend"
32
32
  Requires-Dist: geodatasets; extra == "backend"
33
33
  Requires-Dist: geopandas; extra == "backend"
34
+ Provides-Extra: openai
35
+ Requires-Dist: dasf[openai]; extra == "openai"
36
+ Provides-Extra: broker
37
+ Requires-Dist: dasf[broker]; extra == "broker"
34
38
  Provides-Extra: testsite
35
- Requires-Dist: dasf-exb-reference[backend]; extra == "testsite"
39
+ Requires-Dist: dasf-exb-reference[backend,broker,openai]; extra == "testsite"
36
40
  Requires-Dist: tox; extra == "testsite"
37
41
  Requires-Dist: isort==7.0.0; extra == "testsite"
38
42
  Requires-Dist: black==25.9.0; extra == "testsite"
@@ -40,7 +44,7 @@ Requires-Dist: blackdoc==0.4.6; extra == "testsite"
40
44
  Requires-Dist: flake8==7.3.0; extra == "testsite"
41
45
  Requires-Dist: pre-commit; extra == "testsite"
42
46
  Requires-Dist: mypy==1.18.2; extra == "testsite"
43
- Requires-Dist: dasf-broker-django>=0.0.1.dev0; extra == "testsite"
47
+ Requires-Dist: dasf-broker-django[pytest]>=1.0.0; extra == "testsite"
44
48
  Requires-Dist: djangorestframework; extra == "testsite"
45
49
  Requires-Dist: pytest-django; extra == "testsite"
46
50
  Requires-Dist: pytest-cov; extra == "testsite"
@@ -8,11 +8,11 @@ import json
8
8
 
9
9
  version_json = '''
10
10
  {
11
- "date": "2026-05-12T14:50:48+0200",
11
+ "date": "2026-11-02T10:44:05+0100",
12
12
  "dirty": false,
13
13
  "error": null,
14
- "full-revisionid": "15b6443ca5d9dae4264837b9cf0fde3d00236c95",
15
- "version": "0.1.0"
14
+ "full-revisionid": "28b10ed9b9c275ca3d4b2a024ed41fa1d2ddd6ec",
15
+ "version": "0.2.0"
16
16
  }
17
17
  ''' # END VERSION_JSON
18
18
 
@@ -11,14 +11,16 @@ This backend module provides some demo functions for demonstrating the
11
11
 
12
12
  import datetime as dt
13
13
  import os
14
- from typing import TYPE_CHECKING, Dict, List, Literal
14
+ from typing import TYPE_CHECKING, Any, Dict, List, Literal
15
15
 
16
16
  import geodatasets
17
17
  import geopandas as gpd
18
18
  import pandas as pd
19
- from demessaging import configure, main, registry # noqa: F401
19
+ from dasf import configure, main, registry # noqa: F401
20
+ from dasf.reports import InputReport
20
21
 
21
22
  from .input_models import GeoArea, GeoLocation
23
+ from .input_module import GeoAreaProperty, GeoLocationProperty
22
24
  from .return_models import (
23
25
  BinaryFile,
24
26
  DataSource,
@@ -37,6 +39,7 @@ __all__ = [
37
39
  "get_timeseries",
38
40
  "show_buttons",
39
41
  "get_multiple_responses",
42
+ "InputReporterDemo",
40
43
  ]
41
44
 
42
45
 
@@ -99,7 +102,7 @@ def get_geodataset(what: T_WHAT) -> GeoJson:
99
102
  """
100
103
  path = geodatasets.get_path(what)
101
104
  df = gpd.read_file(path)
102
- return GeoJson(data=df.to_crs("EPSG: 4326").to_geo_dict(), title=what)
105
+ return GeoJson(data=df.to_crs("epsg:4326").to_geo_dict(), title=what)
103
106
 
104
107
 
105
108
  def get_point_info(
@@ -222,12 +225,38 @@ def get_multiple_responses() -> MultipleResponses:
222
225
  )
223
226
 
224
227
 
228
+ class InputReporterDemo:
229
+ """Demo for asking the user for a location and an area on a map."""
230
+
231
+ def input_report_geo_demo(self) -> Dict[str, Any]:
232
+ """Ask the user for a location and an area on the map
233
+
234
+ Returns
235
+ -------
236
+ Dict[str, Any]
237
+ The response of the user, with the ``location`` (see
238
+ :class:`~dasf_exb.input_models.GeoLocation`) and the ``area`` (see
239
+ :class:`~dasf_exb.input_models.GeoArea`) that the user selected.
240
+ """
241
+ properties = {
242
+ "location": GeoLocationProperty(
243
+ description="Select a location on the map"
244
+ ),
245
+ "area": GeoAreaProperty(description="Select an area on the map"),
246
+ }
247
+ with InputReport() as reporter:
248
+ answer = reporter.ask_for_properties(
249
+ properties, "Select a location and an area"
250
+ )
251
+ return answer
252
+
253
+
225
254
  if __name__ == "__main__":
226
255
  if os.getenv("DE_MESSAGING_ENV_FILE"):
227
- from demessaging.config import BaseMessagingConfig, ModuleConfig
256
+ from dasf.config import MessagingConfig, ModuleConfig
228
257
 
229
258
  config = ModuleConfig(
230
- messaging_config=BaseMessagingConfig( # type: ignore[arg-type]
259
+ messaging_config=MessagingConfig( # type: ignore[arg-type]
231
260
  topic="experience-builder-demo",
232
261
  _env_file=os.environ["DE_MESSAGING_ENV_FILE"],
233
262
  )
@@ -21,7 +21,7 @@ def download_geodatasets():
21
21
  for key in geodatasets.data.flatten().keys():
22
22
  try:
23
23
  geodatasets.get_path(key)
24
- except HTTPError as e:
24
+ except (ValueError, HTTPError) as e:
25
25
  print("Failed to download %s dataset: %s" % (key, e))
26
26
 
27
27
 
@@ -0,0 +1,177 @@
1
+ # SPDX-FileCopyrightText: 2026 Helmholtz-Zentrum hereon GmbH
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ # This module is a backend module for the LLM tools of a chat, that extends
6
+ # :mod:`dasf.utility_modules.input_module` with the
7
+ # :class:`GeoLocationProperty` and the :class:`GeoAreaProperty`, to let the
8
+ # user select a point or an area on a map. See there for how to use it. Pass
9
+ # ``"dasf_exb.input_module"`` instead of
10
+ # ``"dasf.utility_modules.input_module"`` to
11
+ # :meth:`dasf.BackendModule.create_model`. The docstrings in this module are
12
+ # the descriptions that the LLM gets for the tools.
13
+
14
+ import inspect
15
+ import textwrap
16
+ import threading
17
+ from typing import (
18
+ Any,
19
+ ClassVar,
20
+ Dict,
21
+ Literal,
22
+ Mapping,
23
+ Optional,
24
+ Type,
25
+ Union,
26
+ cast,
27
+ )
28
+
29
+ from dasf import configure
30
+ from dasf.reports.input import BaseProperty
31
+ from dasf.utility_modules import input_module as dasf_input_module
32
+ from pydantic import BaseModel, Field
33
+
34
+ from .input_models import GeoArea, GeoLocation
35
+
36
+ __doc__ = dasf_input_module.__doc__
37
+
38
+ __all__ = ["ask_for_input"]
39
+
40
+
41
+ def _model_schema(model: Type[BaseModel]) -> Dict[str, Any]:
42
+ """Get the JSON-Schema of `model` without any ``$ref``.
43
+
44
+ The schema of a property is embedded into the schema of the question, so
45
+ the ``$defs`` of the model would not be found at the root of the document.
46
+ """
47
+ schema = model.model_json_schema()
48
+ defs = schema.pop("$defs", {})
49
+
50
+ def resolve(node: Any) -> Any:
51
+ if isinstance(node, list):
52
+ return [resolve(item) for item in node]
53
+ if not isinstance(node, dict):
54
+ return node
55
+ resolved = {
56
+ key: resolve(value) for key, value in node.items() if key != "$ref"
57
+ }
58
+ if "$ref" in node:
59
+ resolved = {
60
+ **resolve(defs[node["$ref"].rsplit("/", 1)[-1]]),
61
+ **resolved,
62
+ }
63
+ return resolved
64
+
65
+ return resolve(schema)
66
+
67
+
68
+ class _GeoProperty(BaseProperty):
69
+ """Base class for the properties that the user selects on a map.
70
+
71
+ The :attr:`~BaseProperty.type` of these properties is not a JSON-Schema
72
+ type, but tells them apart from each other. The user cannot choose from a
73
+ list of values here, but selects the value on a map.
74
+ """
75
+
76
+ #: The model that the answer of the user must match
77
+ _geo_model: ClassVar[Type[BaseModel]]
78
+
79
+ choices: Literal[None] = Field( # type: ignore
80
+ None, description="This is disabled for properties on a map."
81
+ )
82
+
83
+ accepts_other: Literal[False] = Field( # type: ignore
84
+ False, description="This is disabled for properties on a map."
85
+ )
86
+
87
+ def to_json_schema(self) -> Dict[str, Any]:
88
+ """Get the JSON-Schema of this property.
89
+
90
+ This is the schema of the model that the property is selected for. It
91
+ has the ``uiSchema`` of this model, that makes the frontend show a
92
+ map to the user.
93
+ """
94
+ schema = _model_schema(self._geo_model)
95
+ if self.description:
96
+ schema["description"] = self.description
97
+ if self.default is not None:
98
+ schema["default"] = cast(BaseModel, self.default).model_dump()
99
+ return schema
100
+
101
+
102
+ class GeoLocationProperty(_GeoProperty):
103
+ """A location on a map that the user is asked for.
104
+
105
+ The answer is an object with the longitude (``lon``) and the latitude
106
+ (``lat``) of the point that the user selected.
107
+ """
108
+
109
+ _geo_model: ClassVar[Type[BaseModel]] = GeoLocation
110
+
111
+ type: Literal["geolocation"] = Field( # type: ignore
112
+ "geolocation", description="The type of the property."
113
+ )
114
+
115
+ default: Optional[GeoLocation] = None
116
+
117
+
118
+ class GeoAreaProperty(_GeoProperty):
119
+ """An area on a map that the user is asked for.
120
+
121
+ The answer is an object with the ``points`` (each of them with a
122
+ longitude ``lon`` and a latitude ``lat``) of the polygon that the user
123
+ selected.
124
+ """
125
+
126
+ _geo_model: ClassVar[Type[BaseModel]] = GeoArea
127
+
128
+ type: Literal["geoarea"] = Field( # type: ignore
129
+ "geoarea", description="The type of the property."
130
+ )
131
+
132
+ default: Optional[GeoArea] = None
133
+
134
+
135
+ #: The properties that the user can be asked for
136
+ Property = Union[
137
+ dasf_input_module.Property, GeoLocationProperty, GeoAreaProperty
138
+ ]
139
+
140
+
141
+ @configure(cancelable=True)
142
+ def ask_for_input(
143
+ cancel_event: Optional[threading.Event],
144
+ prompt: str,
145
+ properties: Mapping[str, Property],
146
+ ) -> Dict[str, Any]:
147
+ # the docstring is set below
148
+ return dasf_input_module.ask_for_input(
149
+ cancel_event, prompt, properties # type: ignore[arg-type]
150
+ )
151
+
152
+
153
+ # The docstring is what the LLM gets as the description of the tool. It is the
154
+ # one of the dasf function, extended by the description of the geo properties
155
+ # at the end of the description of the `properties`, i.e. before the section
156
+ # with the return value.
157
+ _head, _sep, _tail = inspect.cleandoc(
158
+ dasf_input_module.ask_for_input.__doc__ or ""
159
+ ).partition("\nReturns\n")
160
+ ask_for_input.__doc__ = (
161
+ _head.rstrip()
162
+ + "\n"
163
+ + textwrap.indent(
164
+ textwrap.fill(
165
+ "In addition to these types, the ``type`` of a property can be "
166
+ '``"geolocation"`` or ``"geoarea"``. The user then selects a '
167
+ "point or an area on a map. The answer is an object with ``lon`` "
168
+ "and ``lat`` for the point, and an object with a list of such "
169
+ "``points`` for the area. These have neither ``choices`` nor "
170
+ "``accepts_other``.",
171
+ width=75,
172
+ ),
173
+ " ",
174
+ )
175
+ + _sep
176
+ + _tail
177
+ )
@@ -135,9 +135,9 @@ class DataSource(BaseModel):
135
135
 
136
136
  response_type: Literal["datasource"] = "datasource"
137
137
 
138
- records: List[Dict[str, Union[str, dt.datetime, dt.date, float, int]]] = (
139
- Field(description="The individual record information.")
140
- )
138
+ records: List[
139
+ Dict[str, Union[None, str, dt.datetime, dt.date, float, int]]
140
+ ] = Field(description="The individual record information.")
141
141
 
142
142
  name: str = Field(
143
143
  description="Name of the datasource in the frontend application",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dasf-exb-reference
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Reference implementation and Demonstration of a DASF Backend Module visualized by the dasf-experiencebuilder-plugin
5
5
  Author-email: "Philipp S. Sommer" <philipp.sommer@hereon.de>, Markus Benninghoff <markus.benninghoff@hereon.de>
6
6
  Maintainer-email: "Philipp S. Sommer" <philipp.sommer@hereon.de>, Markus Benninghoff <markus.benninghoff@hereon.de>
@@ -26,13 +26,17 @@ Description-Content-Type: text/markdown
26
26
  License-File: LICENSES/Apache-2.0.txt
27
27
  License-File: LICENSES/CC-BY-4.0.txt
28
28
  License-File: LICENSES/CC0-1.0.txt
29
- Requires-Dist: demessaging>=0.4.0
29
+ Requires-Dist: dasf>=1.0.1
30
30
  Provides-Extra: backend
31
- Requires-Dist: demessaging[backend]; extra == "backend"
31
+ Requires-Dist: dasf[backend]; extra == "backend"
32
32
  Requires-Dist: geodatasets; extra == "backend"
33
33
  Requires-Dist: geopandas; extra == "backend"
34
+ Provides-Extra: openai
35
+ Requires-Dist: dasf[openai]; extra == "openai"
36
+ Provides-Extra: broker
37
+ Requires-Dist: dasf[broker]; extra == "broker"
34
38
  Provides-Extra: testsite
35
- Requires-Dist: dasf-exb-reference[backend]; extra == "testsite"
39
+ Requires-Dist: dasf-exb-reference[backend,broker,openai]; extra == "testsite"
36
40
  Requires-Dist: tox; extra == "testsite"
37
41
  Requires-Dist: isort==7.0.0; extra == "testsite"
38
42
  Requires-Dist: black==25.9.0; extra == "testsite"
@@ -40,7 +44,7 @@ Requires-Dist: blackdoc==0.4.6; extra == "testsite"
40
44
  Requires-Dist: flake8==7.3.0; extra == "testsite"
41
45
  Requires-Dist: pre-commit; extra == "testsite"
42
46
  Requires-Dist: mypy==1.18.2; extra == "testsite"
43
- Requires-Dist: dasf-broker-django>=0.0.1.dev0; extra == "testsite"
47
+ Requires-Dist: dasf-broker-django[pytest]>=1.0.0; extra == "testsite"
44
48
  Requires-Dist: djangorestframework; extra == "testsite"
45
49
  Requires-Dist: pytest-django; extra == "testsite"
46
50
  Requires-Dist: pytest-cov; extra == "testsite"
@@ -10,6 +10,7 @@ dasf_exb/_version.py
10
10
  dasf_exb/backend.py
11
11
  dasf_exb/download_geodatasets.py
12
12
  dasf_exb/input_models.py
13
+ dasf_exb/input_module.py
13
14
  dasf_exb/py.typed
14
15
  dasf_exb/return_models.py
15
16
  dasf_exb_reference.egg-info/PKG-INFO
@@ -19,4 +20,6 @@ dasf_exb_reference.egg-info/not-zip-safe
19
20
  dasf_exb_reference.egg-info/requires.txt
20
21
  dasf_exb_reference.egg-info/top_level.txt
21
22
  tests/test_backend.py
22
- tests/test_imports.py
23
+ tests/test_connected_module.py
24
+ tests/test_imports.py
25
+ tests/test_input_module.py
@@ -1,10 +1,13 @@
1
- demessaging>=0.4.0
1
+ dasf>=1.0.1
2
2
 
3
3
  [backend]
4
- demessaging[backend]
4
+ dasf[backend]
5
5
  geodatasets
6
6
  geopandas
7
7
 
8
+ [broker]
9
+ dasf[broker]
10
+
8
11
  [dev]
9
12
  dasf-exb-reference[testsite]
10
13
  dasf-exb-reference[docs]
@@ -18,8 +21,11 @@ hereon-netcdf-sphinxext
18
21
  sphinx-design
19
22
  myst_parser
20
23
 
24
+ [openai]
25
+ dasf[openai]
26
+
21
27
  [testsite]
22
- dasf-exb-reference[backend]
28
+ dasf-exb-reference[backend,broker,openai]
23
29
  tox
24
30
  isort==7.0.0
25
31
  black==25.9.0
@@ -27,7 +33,7 @@ blackdoc==0.4.6
27
33
  flake8==7.3.0
28
34
  pre-commit
29
35
  mypy==1.18.2
30
- dasf-broker-django>=0.0.1.dev0
36
+ dasf-broker-django[pytest]>=1.0.0
31
37
  djangorestframework
32
38
  pytest-django
33
39
  pytest-cov
@@ -47,7 +47,7 @@ classifiers = [
47
47
 
48
48
  requires-python = '>= 3.9'
49
49
  dependencies = [
50
- "demessaging>=0.4.0",
50
+ "dasf>=1.0.1",
51
51
  # add your dependencies here
52
52
  ]
53
53
 
@@ -60,12 +60,21 @@ Tracker = "https://codebase.helmholtz.cloud/dasf/demos/dasf-exb-reference/issues
60
60
 
61
61
  [project.optional-dependencies]
62
62
  backend = [
63
- "demessaging[backend]",
63
+ "dasf[backend]",
64
64
  "geodatasets",
65
65
  "geopandas",
66
66
  ]
67
+
68
+ openai = [
69
+ "dasf[openai]",
70
+ ]
71
+ # everything that the development message broker (the runserver command of the
72
+ # backend module) needs
73
+ broker = [
74
+ "dasf[broker]",
75
+ ]
67
76
  testsite = [
68
- "dasf-exb-reference[backend]",
77
+ "dasf-exb-reference[backend, broker, openai]",
69
78
  "tox",
70
79
  "isort==7.0.0",
71
80
  "black==25.9.0",
@@ -73,7 +82,7 @@ testsite = [
73
82
  "flake8==7.3.0",
74
83
  "pre-commit",
75
84
  "mypy==1.18.2",
76
- "dasf-broker-django>=0.0.1.dev0",
85
+ "dasf-broker-django[pytest]>=1.0.0",
77
86
  "djangorestframework",
78
87
  "pytest-django",
79
88
  "pytest-cov",
@@ -119,7 +128,7 @@ exclude = [
119
128
 
120
129
  [tool.pytest.ini_options]
121
130
  addopts = '-v'
122
- DJANGO_SETTINGS_MODULE = "testproject.settings"
131
+ DJANGO_SETTINGS_MODULE = "dasf.dev_broker.settings"
123
132
 
124
133
  [tool.versioneer]
125
134
  VCS = 'git'
@@ -0,0 +1,123 @@
1
+ # SPDX-FileCopyrightText: 2025 Helmholtz-Zentrum hereon GmbH
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ """Test module for :mod:`dasf_exb.backend`."""
6
+
7
+ import json
8
+
9
+
10
+ def test_show_version():
11
+ from dasf_exb import __version__ as ref_version
12
+ from dasf_exb import backend
13
+
14
+ version_info = backend.version_info()
15
+ assert "dasf-exb-reference" in version_info
16
+ assert version_info["dasf-exb-reference"] == ref_version
17
+
18
+
19
+ def test_get_geodataset():
20
+ from dasf_exb import backend
21
+
22
+ geojson = backend.get_geodataset("naturalearth.land")
23
+ assert geojson is not None
24
+
25
+
26
+ def test_get_point_info():
27
+ from dasf_exb import backend
28
+ from dasf_exb.input_models import GeoLocation
29
+
30
+ text = backend.get_point_info(
31
+ GeoLocation(lat=40, lon=40), [GeoLocation(lat=30, lon=30)]
32
+ )
33
+ assert "lon: 40" in text
34
+ assert "lat: 40" in text
35
+ assert "lon: 30" in text
36
+ assert "lat: 30" in text
37
+
38
+
39
+ def test_get_timeseries():
40
+ import pandas as pd
41
+
42
+ from dasf_exb import backend
43
+
44
+ timeseries = backend.get_timeseries()
45
+
46
+ df = pd.DataFrame.from_dict(timeseries.records)
47
+ assert sorted(df.columns) == ["date", "value", "value2"]
48
+ assert len(df) == 10
49
+
50
+
51
+ def test_get_multiple_responses():
52
+ import pandas as pd
53
+
54
+ from dasf_exb import backend
55
+
56
+ responses = backend.get_multiple_responses()
57
+
58
+ df = pd.DataFrame.from_dict(responses.data[0].records)
59
+ assert sorted(df.columns) == ["date", "value", "value2"]
60
+ assert len(df) == 10
61
+
62
+
63
+ def test_create_backend_module():
64
+ """Test that dasf accepts the module.
65
+
66
+ dasf creates the model for the requests when the backend module is
67
+ created. It rejects, for instance, functions with a report as parameter.
68
+ """
69
+ from dasf import BackendModule
70
+
71
+ Model = BackendModule.create_model(
72
+ "dasf_exb.backend", messaging_config=dict(topic="test")
73
+ )
74
+
75
+ schema = json.dumps(Model.model_json_schema())
76
+ assert "get_point_info" in schema
77
+ assert "InputReporterDemo" in schema
78
+
79
+
80
+ def test_process_point_request():
81
+ """Test a request for a function with the models of this package."""
82
+ from dasf import BackendModule
83
+
84
+ Model = BackendModule.create_model(
85
+ "dasf_exb.backend", messaging_config=dict(topic="test")
86
+ )
87
+
88
+ result = Model.process_request(
89
+ {
90
+ "func_name": "get_point_info",
91
+ "point": {"lon": 40, "lat": 41},
92
+ "points": [{"lon": 30, "lat": 31}],
93
+ }
94
+ )
95
+
96
+ assert "lon: 40" in result.root
97
+ assert "lat: 41" in result.root
98
+ assert "lon: 30" in result.root
99
+
100
+
101
+ def test_input_report_geo_demo(asked_questions):
102
+ """Test that the demo asks for a location and an area on the map."""
103
+ from dasf import BackendModule
104
+
105
+ from dasf_exb.input_module import GeoAreaProperty, GeoLocationProperty
106
+
107
+ Model = BackendModule.create_model(
108
+ "dasf_exb.backend", messaging_config=dict(topic="test")
109
+ )
110
+
111
+ result = Model.process_request(
112
+ {
113
+ "class_name": "InputReporterDemo",
114
+ "function": {"func_name": "input_report_geo_demo"},
115
+ }
116
+ )
117
+
118
+ assert result.root == {"location": "location", "area": "area"}
119
+ assert len(asked_questions) == 1
120
+ properties, prompt = asked_questions[0]
121
+ assert prompt == "Select a location and an area"
122
+ assert isinstance(properties["location"], GeoLocationProperty)
123
+ assert isinstance(properties["area"], GeoAreaProperty)
@@ -0,0 +1,41 @@
1
+ # SPDX-FileCopyrightText: 2026 Helmholtz-Zentrum hereon GmbH
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ """Tests for the backend module that is connected to a message broker."""
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ from typing import TYPE_CHECKING, Callable, Dict, Union
11
+
12
+ if TYPE_CHECKING:
13
+ import subprocess as spr
14
+
15
+
16
+ def test_request_via_broker(
17
+ connected_module: spr.Popen,
18
+ random_topic: str,
19
+ test_dasf_request: Callable[[str, str, Union[Dict, str]], str],
20
+ ):
21
+ """Send a request to the module via a message broker.
22
+
23
+ The module runs in a process of its own, that is connected to a live
24
+ development broker (see the ``connected_module`` fixture).
25
+ """
26
+ assert connected_module.poll() is None
27
+
28
+ output = test_dasf_request(
29
+ random_topic,
30
+ "dasf_exb.backend",
31
+ {
32
+ "func_name": "get_point_info",
33
+ "point": {"lon": 40, "lat": 41},
34
+ "points": [{"lon": 30, "lat": 31}],
35
+ },
36
+ )
37
+
38
+ text = json.loads(output)
39
+ assert "lon: 40" in text
40
+ assert "lat: 41" in text
41
+ assert "lon: 30" in text
@@ -0,0 +1,226 @@
1
+ # SPDX-FileCopyrightText: 2026 Helmholtz-Zentrum hereon GmbH
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ """Test module for :mod:`dasf_exb.input_module`."""
6
+
7
+ import json
8
+ import threading
9
+ from typing import Any, Dict, Type
10
+
11
+ import docstring_parser
12
+ import jsonschema
13
+ import pytest
14
+ from dasf import BackendModule
15
+ from dasf.reports.input import (
16
+ BaseProperty,
17
+ BooleanProperty,
18
+ FloatProperty,
19
+ IntegerProperty,
20
+ StringProperty,
21
+ properties_to_schema,
22
+ )
23
+ from pydantic import TypeAdapter, ValidationError
24
+
25
+ from dasf_exb import input_module
26
+ from dasf_exb.input_models import GeoLocation
27
+ from dasf_exb.input_module import (
28
+ GeoAreaProperty,
29
+ GeoLocationProperty,
30
+ Property,
31
+ ask_for_input,
32
+ )
33
+
34
+ TOPIC = "user_input"
35
+
36
+
37
+ @pytest.fixture
38
+ def module() -> Type[BackendModule]:
39
+ return BackendModule.create_model(
40
+ "dasf_exb.input_module", messaging_config=dict(topic=TOPIC)
41
+ )
42
+
43
+
44
+ def test_single_function():
45
+ assert input_module.__all__ == ["ask_for_input"]
46
+ assert callable(ask_for_input)
47
+
48
+
49
+ class TestGeoProperties:
50
+ def test_location_schema(self):
51
+ schema = GeoLocationProperty(description="Pick").to_json_schema()
52
+
53
+ # the schema is embedded into the one of the question, so there must
54
+ # not be a reference to the definitions
55
+ assert "$ref" not in json.dumps(schema)
56
+ assert "$defs" not in schema
57
+ assert schema["description"] == "Pick"
58
+ assert schema["uiSchema"]["ui:field"] == "GeoLocation"
59
+ assert sorted(schema["properties"]) == ["lat", "lon"]
60
+ assert sorted(schema["required"]) == ["lat", "lon"]
61
+
62
+ def test_location_default(self):
63
+ prop = GeoLocationProperty(default=GeoLocation(lon=10, lat=20))
64
+ assert prop.to_json_schema()["default"] == {"lon": 10.0, "lat": 20.0}
65
+
66
+ def test_area_schema(self):
67
+ schema = GeoAreaProperty(description="Pick an area").to_json_schema()
68
+
69
+ assert "$ref" not in json.dumps(schema)
70
+ assert schema["description"] == "Pick an area"
71
+ assert schema["uiSchema"]["ui:field"] == "GeoArea"
72
+ # the points are resolved, too
73
+ points = schema["properties"]["points"]["items"]
74
+ assert sorted(points["properties"]) == ["lat", "lon"]
75
+
76
+ @pytest.mark.parametrize(
77
+ "kws", [{"choices": ["a"]}, {"accepts_other": True}]
78
+ )
79
+ @pytest.mark.parametrize("cls", [GeoLocationProperty, GeoAreaProperty])
80
+ def test_no_choices(self, cls: Type[GeoLocationProperty], kws):
81
+ """The user selects on a map, there is nothing to choose from."""
82
+ with pytest.raises(ValidationError):
83
+ cls(**kws)
84
+
85
+ def test_answers_are_validated(self):
86
+ """The answers are validated by dasf against the schema we created."""
87
+ properties = {
88
+ "name": StringProperty(description="Your name"),
89
+ "location": GeoLocationProperty(description="Pick"),
90
+ "area": GeoAreaProperty(description="Pick an area"),
91
+ }
92
+ schema = properties_to_schema(properties)
93
+ point = {"lon": 10, "lat": 20}
94
+
95
+ jsonschema.validate(
96
+ {"name": "Peter", "location": point, "area": {"points": [point]}},
97
+ schema,
98
+ )
99
+
100
+ with pytest.raises(jsonschema.ValidationError):
101
+ jsonschema.validate(
102
+ {
103
+ "name": "Peter",
104
+ "location": {"lon": 10, "lat": 91}, # not a latitude
105
+ "area": {"points": [point]},
106
+ },
107
+ schema,
108
+ )
109
+
110
+
111
+ class TestProperty:
112
+ @pytest.mark.parametrize(
113
+ "type_, cls",
114
+ [
115
+ ("integer", IntegerProperty),
116
+ ("number", FloatProperty),
117
+ ("string", StringProperty),
118
+ ("boolean", BooleanProperty),
119
+ ("geolocation", GeoLocationProperty),
120
+ ("geoarea", GeoAreaProperty),
121
+ ],
122
+ )
123
+ def test_union(self, type_: str, cls: type):
124
+ """The ``type`` of a property tells which one it is."""
125
+ prop: BaseProperty = TypeAdapter(Property).validate_python(
126
+ {"type": type_, "description": "something"}
127
+ )
128
+ assert type(prop) is cls
129
+
130
+ def test_unknown_type(self):
131
+ with pytest.raises(ValidationError):
132
+ TypeAdapter(Property).validate_python({"type": "other"})
133
+
134
+
135
+ class TestAskForInput:
136
+ def test_docstring(self):
137
+ """The LLM gets the description of the geo properties."""
138
+ doc = docstring_parser.parse(ask_for_input.__doc__ or "")
139
+
140
+ params = {param.arg_name: param for param in doc.params}
141
+ assert sorted(params) == ["prompt", "properties"]
142
+ description = params["properties"].description or ""
143
+ # the description of dasf is still there ...
144
+ assert "accepts_other" in description
145
+ # ... and we added ours
146
+ assert '"geolocation"' in description
147
+ assert '"geoarea"' in description
148
+ # and it ends up in the description of the parameter, not of the
149
+ # return value
150
+ assert doc.returns is not None
151
+ assert "geo" not in (doc.returns.description or "")
152
+
153
+ def test_schema(self, module: Type[BackendModule]):
154
+ """The LLM can choose the geo properties and has no reporter."""
155
+ defs: Dict[str, Any] = module.model_json_schema()["$defs"]
156
+
157
+ assert "GeoLocationProperty" in defs
158
+ assert "GeoAreaProperty" in defs
159
+ (func,) = [
160
+ func
161
+ for func in defs.values()
162
+ if func.get("properties", {}).get("func_name", {}).get("const")
163
+ == "ask_for_input"
164
+ ]
165
+ assert sorted(func["properties"]) == [
166
+ "func_name",
167
+ "prompt",
168
+ "properties",
169
+ ]
170
+ variants = func["properties"]["properties"]["additionalProperties"][
171
+ "anyOf"
172
+ ]
173
+ assert {"$ref": "#/$defs/GeoLocationProperty"} in variants
174
+ assert {"$ref": "#/$defs/GeoAreaProperty"} in variants
175
+
176
+ def test_request(self, module: Type[BackendModule], asked_questions):
177
+ """Test the request of the LLM with properties on a map."""
178
+ result = module.process_request(
179
+ {
180
+ "func_name": "ask_for_input",
181
+ "prompt": "Where are you?",
182
+ "properties": {
183
+ "name": {"type": "string", "description": "Your name"},
184
+ "location": {
185
+ "type": "geolocation",
186
+ "description": "Your location",
187
+ },
188
+ "area": {"type": "geoarea", "description": "Your area"},
189
+ },
190
+ }
191
+ )
192
+
193
+ assert result.root == {
194
+ "name": "name",
195
+ "location": "location",
196
+ "area": "area",
197
+ }
198
+ assert len(asked_questions) == 1
199
+ properties, prompt = asked_questions[0]
200
+ assert prompt == "Where are you?"
201
+ assert isinstance(properties["name"], StringProperty)
202
+ assert isinstance(properties["location"], GeoLocationProperty)
203
+ assert properties["location"].description == "Your location"
204
+ assert isinstance(properties["area"], GeoAreaProperty)
205
+
206
+ def test_cancel(self, monkeypatch: pytest.MonkeyPatch, asked_questions):
207
+ """The event to cancel the question gets to the question."""
208
+ from dasf.reports import InputReport
209
+
210
+ stop_events = []
211
+ ask_for_properties = InputReport.ask_for_properties
212
+
213
+ def spy(self, properties, prompt=None, stop_event=None, *args):
214
+ stop_events.append(stop_event)
215
+ return ask_for_properties(self, properties, prompt, stop_event)
216
+
217
+ monkeypatch.setattr(InputReport, "ask_for_properties", spy)
218
+ cancel_event = threading.Event()
219
+
220
+ ask_for_input(
221
+ cancel_event,
222
+ "Where are you?",
223
+ {"location": GeoLocationProperty(description="Your location")},
224
+ )
225
+
226
+ assert stop_events == [cancel_event]
@@ -1,58 +0,0 @@
1
- # SPDX-FileCopyrightText: 2025 Helmholtz-Zentrum hereon GmbH
2
- #
3
- # SPDX-License-Identifier: Apache-2.0
4
-
5
- """Test module for :mod:`dasf_exb.backend`."""
6
-
7
-
8
- def test_show_version():
9
- from dasf_exb import __version__ as ref_version
10
- from dasf_exb import backend
11
-
12
- version_info = backend.version_info()
13
- assert "dasf-exb-reference" in version_info
14
- assert version_info["dasf-exb-reference"] == ref_version
15
-
16
-
17
- def test_get_geodataset():
18
- from dasf_exb import backend
19
-
20
- geojson = backend.get_geodataset("naturalearth.land")
21
- assert geojson is not None
22
-
23
-
24
- def test_get_point_info():
25
- from dasf_exb import backend
26
- from dasf_exb.input_models import GeoLocation
27
-
28
- text = backend.get_point_info(
29
- GeoLocation(lat=40, lon=40), [GeoLocation(lat=30, lon=30)]
30
- )
31
- assert "lon: 40" in text
32
- assert "lat: 40" in text
33
- assert "lon: 30" in text
34
- assert "lat: 30" in text
35
-
36
-
37
- def test_get_timeseries():
38
- import pandas as pd
39
-
40
- from dasf_exb import backend
41
-
42
- timeseries = backend.get_timeseries()
43
-
44
- df = pd.DataFrame.from_dict(timeseries.records)
45
- assert sorted(df.columns) == ["date", "value", "value2"]
46
- assert len(df) == 10
47
-
48
-
49
- def test_get_multiple_responses():
50
- import pandas as pd
51
-
52
- from dasf_exb import backend
53
-
54
- responses = backend.get_multiple_responses()
55
-
56
- df = pd.DataFrame.from_dict(responses.data[0].records)
57
- assert sorted(df.columns) == ["date", "value", "value2"]
58
- assert len(df) == 10