nt2gui 0.1.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 (55) hide show
  1. nt2gui-0.1.0/.gitignore +229 -0
  2. nt2gui-0.1.0/PKG-INFO +56 -0
  3. nt2gui-0.1.0/README.md +35 -0
  4. nt2gui-0.1.0/hatch_build.py +17 -0
  5. nt2gui-0.1.0/nt2gui/__init__.py +8 -0
  6. nt2gui-0.1.0/nt2gui/__main__.py +5 -0
  7. nt2gui-0.1.0/nt2gui/api/__init__.py +12 -0
  8. nt2gui-0.1.0/nt2gui/api/common.py +69 -0
  9. nt2gui-0.1.0/nt2gui/api/fields.py +59 -0
  10. nt2gui-0.1.0/nt2gui/api/particles.py +62 -0
  11. nt2gui-0.1.0/nt2gui/api/simulations.py +24 -0
  12. nt2gui-0.1.0/nt2gui/api/state.py +79 -0
  13. nt2gui-0.1.0/nt2gui/app.py +51 -0
  14. nt2gui-0.1.0/nt2gui/cli.py +108 -0
  15. nt2gui-0.1.0/nt2gui/config.py +14 -0
  16. nt2gui-0.1.0/nt2gui/data/__init__.py +0 -0
  17. nt2gui-0.1.0/nt2gui/data/arrays.py +9 -0
  18. nt2gui-0.1.0/nt2gui/data/expressions.py +217 -0
  19. nt2gui-0.1.0/nt2gui/data/metadata.py +110 -0
  20. nt2gui-0.1.0/nt2gui/data/particles.py +212 -0
  21. nt2gui-0.1.0/nt2gui/data/quantities.py +149 -0
  22. nt2gui-0.1.0/nt2gui/errors.py +27 -0
  23. nt2gui-0.1.0/nt2gui/plots/__init__.py +5 -0
  24. nt2gui-0.1.0/nt2gui/plots/line.py +74 -0
  25. nt2gui-0.1.0/nt2gui/plots/particle_histogram.py +17 -0
  26. nt2gui-0.1.0/nt2gui/plots/particle_phase.py +37 -0
  27. nt2gui-0.1.0/nt2gui/plots/pseudocolor.py +76 -0
  28. nt2gui-0.1.0/nt2gui/render.py +103 -0
  29. nt2gui-0.1.0/nt2gui/session/__init__.py +0 -0
  30. nt2gui-0.1.0/nt2gui/session/files.py +106 -0
  31. nt2gui-0.1.0/nt2gui/session/schema.py +240 -0
  32. nt2gui-0.1.0/nt2gui/session/store.py +112 -0
  33. nt2gui-0.1.0/nt2gui/sources/__init__.py +92 -0
  34. nt2gui-0.1.0/nt2gui/sources/cache.py +27 -0
  35. nt2gui-0.1.0/nt2gui/sources/demo.py +144 -0
  36. nt2gui-0.1.0/nt2gui/sources/nt2.py +158 -0
  37. nt2gui-0.1.0/nt2gui/static/assets/index-CMkhsKnH.js +21 -0
  38. nt2gui-0.1.0/nt2gui/static/assets/index-CqdFF03j.css +1 -0
  39. nt2gui-0.1.0/nt2gui/static/index.html +13 -0
  40. nt2gui-0.1.0/nt2gui/workspace.py +138 -0
  41. nt2gui-0.1.0/pyproject.toml +46 -0
  42. nt2gui-0.1.0/tests/conftest.py +13 -0
  43. nt2gui-0.1.0/tests/helpers.py +22 -0
  44. nt2gui-0.1.0/tests/test_api.py +523 -0
  45. nt2gui-0.1.0/tests/test_cli.py +63 -0
  46. nt2gui-0.1.0/tests/test_expressions.py +147 -0
  47. nt2gui-0.1.0/tests/test_fields.py +219 -0
  48. nt2gui-0.1.0/tests/test_nt2loader.py +136 -0
  49. nt2gui-0.1.0/tests/test_particles.py +196 -0
  50. nt2gui-0.1.0/tests/test_pseudocolor.py +105 -0
  51. nt2gui-0.1.0/tests/test_render.py +67 -0
  52. nt2gui-0.1.0/tests/test_session_files.py +171 -0
  53. nt2gui-0.1.0/tests/test_session_store.py +164 -0
  54. nt2gui-0.1.0/tests/test_sources.py +98 -0
  55. nt2gui-0.1.0/tests/test_summary.py +62 -0
@@ -0,0 +1,229 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py.cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ # Pipfile.lock
96
+
97
+ # UV
98
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ # uv.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ # poetry.lock
109
+ # poetry.toml
110
+
111
+ # pdm
112
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
113
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
114
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
115
+ # pdm.lock
116
+ # pdm.toml
117
+ .pdm-python
118
+ .pdm-build/
119
+
120
+ # pixi
121
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
122
+ # pixi.lock
123
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
124
+ # in the .venv directory. It is recommended not to include this directory in version control.
125
+ .pixi
126
+
127
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
128
+ __pypackages__/
129
+
130
+ # Celery stuff
131
+ celerybeat-schedule
132
+ celerybeat.pid
133
+
134
+ # Redis
135
+ *.rdb
136
+ *.aof
137
+ *.pid
138
+
139
+ # RabbitMQ
140
+ mnesia/
141
+ rabbitmq/
142
+ rabbitmq-data/
143
+
144
+ # ActiveMQ
145
+ activemq-data/
146
+
147
+ # SageMath parsed files
148
+ *.sage.py
149
+
150
+ # Environments
151
+ .env
152
+ .envrc
153
+ .venv
154
+ env/
155
+ venv/
156
+ ENV/
157
+ env.bak/
158
+ venv.bak/
159
+
160
+ # Spyder project settings
161
+ .spyderproject
162
+ .spyproject
163
+
164
+ # Rope project settings
165
+ .ropeproject
166
+
167
+ # mkdocs documentation
168
+ /site
169
+
170
+ # mypy
171
+ .mypy_cache/
172
+ .dmypy.json
173
+ dmypy.json
174
+
175
+ # Pyre type checker
176
+ .pyre/
177
+
178
+ # pytype static type analyzer
179
+ .pytype/
180
+
181
+ # Cython debug symbols
182
+ cython_debug/
183
+
184
+ # PyCharm
185
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
186
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
187
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
188
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
189
+ # .idea/
190
+
191
+ # Abstra
192
+ # Abstra is an AI-powered process automation framework.
193
+ # Ignore directories containing user credentials, local state, and settings.
194
+ # Learn more at https://abstra.io/docs
195
+ .abstra/
196
+
197
+ # Visual Studio Code
198
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
199
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
200
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
201
+ # you could uncomment the following to ignore the entire vscode folder
202
+ # .vscode/
203
+
204
+ # Ruff stuff:
205
+ .ruff_cache/
206
+
207
+ # PyPI configuration file
208
+ .pypirc
209
+
210
+ # Marimo
211
+ marimo/_static/
212
+ marimo/_lsp/
213
+ __marimo__/
214
+
215
+ # Streamlit
216
+ .streamlit/secrets.toml
217
+ node_modules/
218
+ dist/
219
+
220
+ # devenv
221
+ .devenv*
222
+ devenv.local.nix
223
+ .pre-commit-config.yaml
224
+
225
+ # nt2gui session state
226
+ .nt2gui/
227
+
228
+ # Built client, bundled into the Python package
229
+ nt2gui/static/
nt2gui-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,56 @@
1
+ Metadata-Version: 2.5
2
+ Name: nt2gui
3
+ Version: 0.1.0
4
+ Summary: Browser UI for visualizing Entity simulation output, served from where the data lives.
5
+ Project-URL: Repository, https://github.com/haykh/nt2gui
6
+ Author-email: Hayk Hakobyan <hhakobyan@flatironinstitute.org>
7
+ Requires-Python: >=3.10
8
+ Requires-Dist: flask>=3.0
9
+ Requires-Dist: matplotlib>=3.8
10
+ Requires-Dist: nt2py[hdf5]>=1.6
11
+ Requires-Dist: numpy>=1.26
12
+ Requires-Dist: pandas>=2.1
13
+ Requires-Dist: pydantic>=2.5
14
+ Requires-Dist: waitress>=3.0
15
+ Requires-Dist: xarray>=2024.1
16
+ Provides-Extra: dev
17
+ Requires-Dist: build; extra == 'dev'
18
+ Requires-Dist: pyright; extra == 'dev'
19
+ Requires-Dist: pytest>=8; extra == 'dev'
20
+ Description-Content-Type: text/markdown
21
+
22
+ # nt2gui
23
+
24
+ A browser UI for visualizing [Entity](https://github.com/entity-toolkit/entity) simulation output, read with [nt2py](https://github.com/entity-toolkit/nt2py).
25
+
26
+ The server runs where the data lives (e.g. a cluster node), does all the data processing, and serves the UI. Your browser connects to it through an SSH tunnel, so nothing needs to be installed on your laptop.
27
+
28
+ ## Usage
29
+
30
+ On the cluster:
31
+
32
+ ```bash
33
+ python -m venv ~/nt2gui-env
34
+ ~/nt2gui-env/bin/pip install nt2gui
35
+ ~/nt2gui-env/bin/nt2gui serve # --port, --host, --state-file, --threads
36
+ ```
37
+
38
+ It prints the tunnel command to run on your machine, e.g.
39
+
40
+ ```bash
41
+ ssh -N -L 8765:localhost:8765 you@node042 # add -J you@login-node if needed
42
+ ```
43
+
44
+ then open <http://localhost:8765/>.
45
+
46
+ By default the server listens on `127.0.0.1` only. The UI session is saved to `.nt2gui/session.json` in the working directory (override with `--state-file` or `$NT2GUI_STATE_FILE`) and restored on restart.
47
+
48
+ ## Development
49
+
50
+ ```bash
51
+ pip install -r requirements.txt # or: pip install -e ".[dev]"
52
+ npm i
53
+ npm run dev # Flask on :5000 and vite with /api proxied to it
54
+ npm run test # vitest + pytest
55
+ npm run package # build the client into nt2gui/static, then the wheel/sdist into dist/
56
+ ```
nt2gui-0.1.0/README.md ADDED
@@ -0,0 +1,35 @@
1
+ # nt2gui
2
+
3
+ A browser UI for visualizing [Entity](https://github.com/entity-toolkit/entity) simulation output, read with [nt2py](https://github.com/entity-toolkit/nt2py).
4
+
5
+ The server runs where the data lives (e.g. a cluster node), does all the data processing, and serves the UI. Your browser connects to it through an SSH tunnel, so nothing needs to be installed on your laptop.
6
+
7
+ ## Usage
8
+
9
+ On the cluster:
10
+
11
+ ```bash
12
+ python -m venv ~/nt2gui-env
13
+ ~/nt2gui-env/bin/pip install nt2gui
14
+ ~/nt2gui-env/bin/nt2gui serve # --port, --host, --state-file, --threads
15
+ ```
16
+
17
+ It prints the tunnel command to run on your machine, e.g.
18
+
19
+ ```bash
20
+ ssh -N -L 8765:localhost:8765 you@node042 # add -J you@login-node if needed
21
+ ```
22
+
23
+ then open <http://localhost:8765/>.
24
+
25
+ By default the server listens on `127.0.0.1` only. The UI session is saved to `.nt2gui/session.json` in the working directory (override with `--state-file` or `$NT2GUI_STATE_FILE`) and restored on restart.
26
+
27
+ ## Development
28
+
29
+ ```bash
30
+ pip install -r requirements.txt # or: pip install -e ".[dev]"
31
+ npm i
32
+ npm run dev # Flask on :5000 and vite with /api proxied to it
33
+ npm run test # vitest + pytest
34
+ npm run package # build the client into nt2gui/static, then the wheel/sdist into dist/
35
+ ```
@@ -0,0 +1,17 @@
1
+ """Refuse to build a wheel without the client, which would serve no UI."""
2
+
3
+ from pathlib import Path
4
+
5
+ from hatchling.builders.hooks.plugin.interface import BuildHookInterface
6
+
7
+
8
+ class CustomBuildHook(BuildHookInterface):
9
+ def initialize(self, version, build_data):
10
+ if version == "editable": # served from the checkout; built on demand
11
+ return
12
+ index = Path(self.root) / "nt2gui" / "static" / "index.html"
13
+ if not index.is_file():
14
+ raise RuntimeError(
15
+ f"{index} is missing: run `npm run build` before building the wheel "
16
+ "(or use `npm run package`)."
17
+ )
@@ -0,0 +1,8 @@
1
+ """nt2gui: a browser UI for Entity (nt2py) simulation output."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ try:
6
+ __version__ = version("nt2gui")
7
+ except PackageNotFoundError: # running from a source checkout
8
+ __version__ = "0.0.0+unknown"
@@ -0,0 +1,5 @@
1
+ import sys
2
+
3
+ from .cli import main
4
+
5
+ sys.exit(main())
@@ -0,0 +1,12 @@
1
+ """The JSON API, served under `/api`: one blueprint per area."""
2
+
3
+ from flask import Blueprint
4
+
5
+ from . import fields, particles, simulations, state
6
+
7
+
8
+ def create_api() -> Blueprint:
9
+ api = Blueprint("api", __name__)
10
+ for area in (state, simulations, fields, particles):
11
+ api.register_blueprint(area.bp)
12
+ return api
@@ -0,0 +1,69 @@
1
+ """Request helpers shared by the API blueprints.
2
+
3
+ Helpers raise `UserError` (400) / `NotFound` (404); the app's error handlers
4
+ turn those, and pydantic `ValidationError`s, into JSON error responses.
5
+ """
6
+
7
+ import json
8
+ from typing import Any, TypeVar
9
+
10
+ import xarray as xr
11
+ from flask import current_app, request
12
+ from pydantic import BaseModel, ValidationError
13
+
14
+ from ..data.particles import particle_dataset
15
+ from ..errors import UserError
16
+ from ..sources import SimulationSource
17
+ from ..workspace import Workspace
18
+
19
+ EXTENSION_KEY = "nt2gui.workspace"
20
+ Model = TypeVar("Model", bound=BaseModel)
21
+
22
+
23
+ def workspace() -> Workspace:
24
+ return current_app.extensions[EXTENSION_KEY]
25
+
26
+
27
+ def simulation(simulation_id: str) -> SimulationSource:
28
+ return workspace().simulation(simulation_id)
29
+
30
+
31
+ def json_object() -> dict[str, Any]:
32
+ """The request body, which must be a JSON object (missing counts as {})."""
33
+ payload = request.get_json(silent=True)
34
+ if payload is None:
35
+ return {}
36
+ if not isinstance(payload, dict):
37
+ raise UserError("Expected a JSON object")
38
+ return payload
39
+
40
+
41
+ def require_time(payload: dict[str, Any]) -> float:
42
+ time = payload.get("time")
43
+ # bool is an int subclass, but never a valid time.
44
+ if isinstance(time, bool) or not isinstance(time, (int, float)):
45
+ raise UserError("Time value is required")
46
+ return float(time)
47
+
48
+
49
+ def require_text(payload: dict[str, Any], key: str, message: str) -> str:
50
+ value = payload.get(key)
51
+ if not isinstance(value, str) or not value.strip():
52
+ raise UserError(message)
53
+ return value.strip()
54
+
55
+
56
+ def parse(model: type[Model], value: Any, what: str) -> Model:
57
+ try:
58
+ return model.model_validate(value)
59
+ except ValidationError as error:
60
+ raise UserError(
61
+ f"Invalid {what}", details=json.loads(error.json(include_url=False))
62
+ ) from error
63
+
64
+
65
+ def particles_at(source: SimulationSource, time: float) -> tuple[xr.Dataset, float]:
66
+ """Particles of the output nearest to `time`, and that output's time."""
67
+ if not source.species:
68
+ raise UserError("This simulation has no particle output")
69
+ return particle_dataset(source.particles(time)), source.particle_time(time)
@@ -0,0 +1,59 @@
1
+ """Field metadata, custom-quantity checks, and field plots."""
2
+
3
+ from flask import Blueprint, jsonify
4
+
5
+ from ..data.metadata import field_metadata
6
+ from ..data.quantities import quantity_dims
7
+ from ..plots.line import render_line
8
+ from ..plots.pseudocolor import render_pseudocolor
9
+ from ..session.schema import LineConfig, PseudocolorConfig, Range, ZoomWindow
10
+ from .common import json_object, parse, require_text, require_time, simulation
11
+
12
+ bp = Blueprint("fields", __name__, url_prefix="/simulations/<simulation_id>")
13
+
14
+
15
+ @bp.get("/fields")
16
+ def get_fields(simulation_id: str):
17
+ return jsonify(field_metadata(simulation(simulation_id).fields))
18
+
19
+
20
+ @bp.post("/quantities/check")
21
+ def check_quantity(simulation_id: str):
22
+ """Validate a field name or expression and report its spatial dims."""
23
+ source = simulation(simulation_id)
24
+ expression = require_text(json_object(), "expression", "An expression is required")
25
+ dims = quantity_dims(expression, source.fields)
26
+ return jsonify({"expression": expression, "dims": dims})
27
+
28
+
29
+ @bp.post("/render/line")
30
+ def line_plot(simulation_id: str):
31
+ source = simulation(simulation_id)
32
+ payload = json_object()
33
+ time = require_time(payload)
34
+ config = parse(LineConfig, payload.get("config"), "line plot request")
35
+ value_range = (
36
+ parse(Range, payload["range"], "zoom range").to_wire()
37
+ if payload.get("range") is not None
38
+ else None
39
+ )
40
+ fields = source.fields
41
+ return jsonify(
42
+ render_line(
43
+ fields, config.quantities, config.axis, config.slices, time, value_range
44
+ )
45
+ )
46
+
47
+
48
+ @bp.post("/render/pseudocolor")
49
+ def pseudocolor_plot(simulation_id: str):
50
+ source = simulation(simulation_id)
51
+ payload = json_object()
52
+ time = require_time(payload)
53
+ config = parse(PseudocolorConfig, payload.get("config"), "pseudocolor request")
54
+ viewport = (
55
+ parse(ZoomWindow, payload["viewport"], "zoom window")
56
+ if payload.get("viewport") is not None
57
+ else None
58
+ )
59
+ return jsonify(render_pseudocolor(source.fields, config, time, viewport))
@@ -0,0 +1,62 @@
1
+ """Particle metadata, custom-quantity checks, and particle plots."""
2
+
3
+ from flask import Blueprint, jsonify
4
+
5
+ from ..data.metadata import particle_metadata
6
+ from ..data.particles import particle_dataset, quantity_values
7
+ from ..errors import UserError
8
+ from ..plots.particle_histogram import render_particle_histogram
9
+ from ..plots.particle_phase import render_particle_phase
10
+ from ..session.schema import ParticleHistogramConfig, ParticlePhaseConfig
11
+ from ..sources import first_particles
12
+ from .common import (
13
+ json_object,
14
+ parse,
15
+ particles_at,
16
+ require_text,
17
+ require_time,
18
+ simulation,
19
+ )
20
+
21
+ bp = Blueprint("particles", __name__, url_prefix="/simulations/<simulation_id>")
22
+
23
+ # Expressions are checked on a few particles; validity doesn't depend on values.
24
+ CHECK_SAMPLE_SIZE = 16
25
+
26
+
27
+ @bp.get("/particles")
28
+ def get_particles(simulation_id: str):
29
+ return jsonify(particle_metadata(simulation(simulation_id)))
30
+
31
+
32
+ @bp.post("/particles/check")
33
+ def check_particle_quantity(simulation_id: str):
34
+ source = simulation(simulation_id)
35
+ expression = require_text(json_object(), "expression", "An expression is required")
36
+ if not source.species:
37
+ raise UserError("This simulation has no particle output")
38
+ sample = particle_dataset(first_particles(source).head(CHECK_SAMPLE_SIZE))
39
+ quantity_values(expression, sample)
40
+ return jsonify({"expression": expression})
41
+
42
+
43
+ @bp.post("/render/particles/histogram")
44
+ def histogram_plot(simulation_id: str):
45
+ source = simulation(simulation_id)
46
+ payload = json_object()
47
+ time = require_time(payload)
48
+ config = parse(ParticleHistogramConfig, payload.get("config"), "histogram request")
49
+ particles, timestamp = particles_at(source, time)
50
+ return jsonify(
51
+ {**render_particle_histogram(particles, config), "timestamp": timestamp}
52
+ )
53
+
54
+
55
+ @bp.post("/render/particles/phase")
56
+ def phase_plot(simulation_id: str):
57
+ source = simulation(simulation_id)
58
+ payload = json_object()
59
+ time = require_time(payload)
60
+ config = parse(ParticlePhaseConfig, payload.get("config"), "phase-space request")
61
+ particles, timestamp = particles_at(source, time)
62
+ return jsonify({**render_particle_phase(particles, config), "timestamp": timestamp})
@@ -0,0 +1,24 @@
1
+ """Opening and closing simulations, and what each contains."""
2
+
3
+ from flask import Blueprint, jsonify
4
+
5
+ from ..data.metadata import summarize
6
+ from .common import json_object, require_text, simulation, workspace
7
+
8
+ bp = Blueprint("simulations", __name__)
9
+
10
+
11
+ @bp.post("/simulations")
12
+ def add_simulation():
13
+ path = require_text(json_object(), "path", "Simulation path is required")
14
+ return jsonify(workspace().add(path))
15
+
16
+
17
+ @bp.delete("/simulations/<simulation_id>")
18
+ def remove_simulation(simulation_id: str):
19
+ return jsonify(workspace().remove(simulation_id))
20
+
21
+
22
+ @bp.get("/simulations/<simulation_id>/summary")
23
+ def get_summary(simulation_id: str):
24
+ return jsonify(summarize(simulation(simulation_id)))
@@ -0,0 +1,79 @@
1
+ """The synced UI state, and saving/loading whole sessions."""
2
+
3
+ from flask import Blueprint, current_app, jsonify
4
+
5
+ from ..errors import Conflict, NotFound, UserError
6
+ from ..session.files import (
7
+ parse_document,
8
+ read_document,
9
+ resolve_session_path,
10
+ write_document,
11
+ )
12
+ from ..session.schema import UiState
13
+ from .common import json_object, parse, require_text, workspace
14
+
15
+ bp = Blueprint("state", __name__)
16
+
17
+
18
+ @bp.get("/state")
19
+ def get_state():
20
+ return jsonify(workspace().store.snapshot())
21
+
22
+
23
+ @bp.put("/state")
24
+ def put_state():
25
+ payload = json_object()
26
+ revision = payload.get("revision")
27
+ if isinstance(revision, bool) or not isinstance(revision, int):
28
+ raise UserError("Expected {revision: int, state: object}")
29
+ state = parse(UiState, payload.get("state"), "state")
30
+ return jsonify(workspace().store.replace_state(state, revision))
31
+
32
+
33
+ @bp.get("/session/export")
34
+ def export_session():
35
+ return jsonify(workspace().store.document().to_wire())
36
+
37
+
38
+ @bp.post("/session/import")
39
+ def import_session():
40
+ try:
41
+ document = parse_document(json_object().get("session"))
42
+ except ValueError as error:
43
+ raise UserError(f"Invalid session file: {error}") from error
44
+ return jsonify(workspace().apply_document(document))
45
+
46
+
47
+ @bp.post("/session/save")
48
+ def save_session():
49
+ payload = json_object()
50
+ path = resolve_session_path(
51
+ require_text(payload, "path", "A session file path is required")
52
+ )
53
+ try:
54
+ write_document(
55
+ path, workspace().store.document(), bool(payload.get("overwrite"))
56
+ )
57
+ except FileExistsError as error:
58
+ raise Conflict("File already exists", path=str(path)) from error
59
+ except OSError as error:
60
+ raise UserError(f"Could not write {path}: {error.strerror or error}") from error
61
+ current_app.logger.info("Session saved to %s", path)
62
+ return jsonify({**workspace().store.snapshot(), "path": str(path)})
63
+
64
+
65
+ @bp.post("/session/load")
66
+ def load_session():
67
+ path = resolve_session_path(
68
+ require_text(json_object(), "path", "A session file path is required")
69
+ )
70
+ try:
71
+ document = read_document(path)
72
+ except FileNotFoundError as error:
73
+ raise NotFound("File not found", path=str(path)) from error
74
+ except ValueError as error:
75
+ raise UserError(f"Invalid session file: {error}") from error
76
+ except OSError as error:
77
+ raise UserError(f"Could not read file: {error.strerror or error}") from error
78
+ current_app.logger.info("Session loaded from %s", path)
79
+ return jsonify({**workspace().apply_document(document), "path": str(path)})