starbash 0.3.3__tar.gz → 0.3.4__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.
- {starbash-0.3.3 → starbash-0.3.4}/PKG-INFO +8 -2
- {starbash-0.3.3 → starbash-0.3.4}/README.md +7 -2
- {starbash-0.3.3 → starbash-0.3.4}/pyproject.toml +9 -2
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/processed_target.py +21 -2
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/publish/github.py +145 -5
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/publish/github_service.py +1 -1
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/templates/report/target.md.jinja +20 -5
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/ui/qt/main_window.py +3 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/ui/qt/pages/processing.py +24 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/ui/qt/pages/wizard.py +35 -0
- {starbash-0.3.3 → starbash-0.3.4}/LICENSE +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/__init__.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/aliases.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/analytics.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/app.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/assets/__init__.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/assets/check.png +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/assets/favicon.ico +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/assets/icon.png +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/assets/icon.svg +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/assets/starbash.desktop.in +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/check_version.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/commands/__init__.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/commands/gui.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/commands/info.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/commands/process.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/commands/publish.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/commands/repo.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/commands/select.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/commands/user.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/database.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/defaults/__init__.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/defaults/starbash.toml +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/doit.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/doit_types.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/dwarf3.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/events.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/exception.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/filtering.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/fits.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/interaction.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/linux.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/main.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/os.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/parameters.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/paths.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/preferences.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/processing.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/processing_like.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/publish/__init__.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/publish/credentials.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/publish/github_publish.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/recipes/README.md +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/recipes/__init__.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/recipes/crop.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/recipes/osc.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/recipes/report_registration.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/report.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/rich.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/run_state.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/safety.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/score.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/selection.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/sim_siril/__init__.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/sim_siril/connection.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/sim_siril/enums.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/sim_siril/utility.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/siril/import_registration.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/stage_utils.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/stages.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/templates/__init__.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/templates/repo/master.toml +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/templates/repo/processed.toml +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/templates/report/Gemfile +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/templates/report/README.md.jinja +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/templates/report/__init__.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/templates/report/_config.yml +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/templates/report/default.html +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/templates/report/index.md.jinja +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/templates/target/master.toml +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/templates/target/processed/about.toml +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/templates/target/processed/main.toml +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/templates/target/processed/sessions.toml +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/templates/userconfig.toml +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/toml.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/tool/__init__.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/tool/base.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/tool/context.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/tool/graxpert.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/tool/python.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/tool/rcastro.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/tool/siril.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/tool/starnet.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/ui/__init__.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/ui/cli.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/ui/cli_events.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/ui/qt/__init__.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/ui/qt/app.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/ui/qt/bridge.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/ui/qt/desktop.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/ui/qt/interaction.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/ui/qt/jobs.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/ui/qt/models.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/ui/qt/pages/__init__.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/ui/qt/pages/base.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/ui/qt/pages/dashboard.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/ui/qt/pages/masters.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/ui/qt/pages/publish.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/ui/qt/pages/repositories.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/ui/qt/pages/sessions.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/ui/qt/pages/settings.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/ui/qt/pages/targets.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/ui/qt/services.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/ui/qt/theme.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/ui/qt/widgets/__init__.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/ui/qt/widgets/busy_indicator.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/ui/qt/widgets/file_links.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/ui/qt/widgets/github_login.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/ui/qt/widgets/hover_preview.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/ui/qt/widgets/image_viewer.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/ui/qt/widgets/log_view.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/ui/qt/widgets/master_picker.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/ui/qt/widgets/selection_panel.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/ui/qt/widgets/stat_card.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/ui/qt/widgets/tool_warning.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/ui/qt/workers.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/url.py +0 -0
- {starbash-0.3.3 → starbash-0.3.4}/src/starbash/windows.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: starbash
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.4
|
|
4
4
|
Summary: A tool for automating/standardizing/sharing astrophotography workflows.
|
|
5
5
|
License-File: LICENSE
|
|
6
6
|
Author: Kevin Hester
|
|
@@ -79,7 +79,12 @@ This project is still very young - but making good progress 😊!
|
|
|
79
79
|
|
|
80
80
|
If you are interested in alpha-testing we ❤️ you. This README should have enough instructions to get you going, but if you encounter **any** problems please file a github issue and we'll work together to fix them.
|
|
81
81
|
|
|
82
|
-
!
|
|
82
|
+
(new!) We now have a full GUI (click the video to play)...
|
|
83
|
+
|
|
84
|
+
<video src="https://github.com/user-attachments/assets/0791c47f-f8fa-4279-be06-f869d7212d8c" autoplay controls loop muted playsinline width="100%"></video>
|
|
85
|
+
|
|
86
|
+
We have a CLI interface also...
|
|
87
|
+

|
|
83
88
|
|
|
84
89
|
## Current features
|
|
85
90
|
|
|
@@ -321,3 +326,4 @@ However, in some of my other open-source projects, I've seen the current hell PR
|
|
|
321
326
|
|
|
322
327
|
Copyright 2025 Kevin Hester, kevinh@geeksville.com.
|
|
323
328
|
Licensed under the [GPL v3](https://raw.githubusercontent.com/geeksville/starbash/refs/heads/main/LICENSE)
|
|
329
|
+
|
|
@@ -46,7 +46,12 @@ This project is still very young - but making good progress 😊!
|
|
|
46
46
|
|
|
47
47
|
If you are interested in alpha-testing we ❤️ you. This README should have enough instructions to get you going, but if you encounter **any** problems please file a github issue and we'll work together to fix them.
|
|
48
48
|
|
|
49
|
-
!
|
|
49
|
+
(new!) We now have a full GUI (click the video to play)...
|
|
50
|
+
|
|
51
|
+
<video src="https://github.com/user-attachments/assets/0791c47f-f8fa-4279-be06-f869d7212d8c" autoplay controls loop muted playsinline width="100%"></video>
|
|
52
|
+
|
|
53
|
+
We have a CLI interface also...
|
|
54
|
+

|
|
50
55
|
|
|
51
56
|
## Current features
|
|
52
57
|
|
|
@@ -287,4 +292,4 @@ However, in some of my other open-source projects, I've seen the current hell PR
|
|
|
287
292
|
## License
|
|
288
293
|
|
|
289
294
|
Copyright 2025 Kevin Hester, kevinh@geeksville.com.
|
|
290
|
-
Licensed under the [GPL v3](https://raw.githubusercontent.com/geeksville/starbash/refs/heads/main/LICENSE)
|
|
295
|
+
Licensed under the [GPL v3](https://raw.githubusercontent.com/geeksville/starbash/refs/heads/main/LICENSE)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "starbash"
|
|
3
|
-
version = "0.3.
|
|
3
|
+
version = "0.3.4"
|
|
4
4
|
description = "A tool for automating/standardizing/sharing astrophotography workflows."
|
|
5
5
|
authors = ["Kevin Hester <kevinh@geeksville.com>"]
|
|
6
6
|
readme = "README.md"
|
|
@@ -61,6 +61,7 @@ doit-graph = ">=0.3.0"
|
|
|
61
61
|
# harlequin = "^2.4.1"
|
|
62
62
|
library-skills = "^0.0.19"
|
|
63
63
|
semble = {version = "^0.6.0", extras = ["mcp"]}
|
|
64
|
+
imageio-ffmpeg = "^0.6.0"
|
|
64
65
|
|
|
65
66
|
[tool.poetry.scripts]
|
|
66
67
|
starbash = "starbash.main:app"
|
|
@@ -71,6 +72,7 @@ markers = [
|
|
|
71
72
|
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
|
|
72
73
|
"integration: marks tests as integration tests requiring /test-data (deselect with '-m \"not integration\"')",
|
|
73
74
|
"gui: marks tests that build real Qt widgets (offscreen; deselect with '-m \"not gui\"')",
|
|
75
|
+
"gui_integration: marks the scripted gui_integration movie runs (needs ffmpeg + /test-data; deselect with '-m \"not gui_integration\"')",
|
|
74
76
|
]
|
|
75
77
|
# -n auto enables parallel test execution plugin
|
|
76
78
|
# By default we exclude only slow and integration tests. GUI tests DO run: PySide6
|
|
@@ -80,7 +82,12 @@ markers = [
|
|
|
80
82
|
# --tb=short shows shorter tracebacks
|
|
81
83
|
# -rfE shows summary of failed and error test outcomes at end (not passed)
|
|
82
84
|
# --ignore excludes third-party test directories
|
|
83
|
-
addopts = "-m 'not slow and not integration' -n auto --tb=short -rfE --ignore=GraXpert --ignore=starbash-recipes"
|
|
85
|
+
addopts = "-m 'not slow and not integration and not gui_integration' -n auto --tb=short -rfE --ignore=GraXpert --ignore=starbash-recipes"
|
|
86
|
+
|
|
87
|
+
# Note: the `gui_integration` movies are deselected by default on purpose: they drive a
|
|
88
|
+
# whole user journey (setup wizard -> reindex -> processing run) and take minutes, so they
|
|
89
|
+
# run on demand - `just test-integration-gui`, or `pytest -m gui_integration -n 0`.
|
|
90
|
+
# The marker name uses an underscore so it is usable as `pytest.mark.gui_integration`.
|
|
84
91
|
|
|
85
92
|
# Note: To limit captured output further, you can:
|
|
86
93
|
# 1. Use --tb=line for minimal tracebacks
|
|
@@ -85,6 +85,10 @@ class StageOption:
|
|
|
85
85
|
parameters: list[ParameterOption]
|
|
86
86
|
#: URL (``https://`` or ``file://``) of the recipe that declares this stage.
|
|
87
87
|
recipe_url: str | None = None
|
|
88
|
+
#: Tool this stage runs (e.g. ``"siril"``), as declared by the recipe.
|
|
89
|
+
tool: str | None = None
|
|
90
|
+
#: Interchangeable role (e.g. ``"stack"``) the recipe declares, if any.
|
|
91
|
+
role: str | None = None
|
|
88
92
|
|
|
89
93
|
|
|
90
94
|
@dataclass
|
|
@@ -189,7 +193,8 @@ def stage_declarations(recipes: Any) -> dict[str, dict[str, Any]]:
|
|
|
189
193
|
Recipes are the authoritative source of a parameter's default and description
|
|
190
194
|
(declared as ``[[stages.parameters]]``). Scanning the recipe repos is cheap
|
|
191
195
|
because their config is already loaded; later definitions win, mirroring repo
|
|
192
|
-
precedence.
|
|
196
|
+
precedence. The stage's declared ``tool`` and optional ``role`` are carried
|
|
197
|
+
along too, for report rendering.
|
|
193
198
|
"""
|
|
194
199
|
declarations: dict[str, dict[str, Any]] = {}
|
|
195
200
|
for repo in recipes:
|
|
@@ -205,7 +210,14 @@ def stage_declarations(recipes: Any) -> dict[str, dict[str, Any]]:
|
|
|
205
210
|
if not name:
|
|
206
211
|
continue
|
|
207
212
|
entry = declarations.setdefault(
|
|
208
|
-
str(name),
|
|
213
|
+
str(name),
|
|
214
|
+
{
|
|
215
|
+
"description": None,
|
|
216
|
+
"parameters": {},
|
|
217
|
+
"recipe_url": None,
|
|
218
|
+
"tool": None,
|
|
219
|
+
"role": None,
|
|
220
|
+
},
|
|
209
221
|
)
|
|
210
222
|
if stage.get("description"):
|
|
211
223
|
entry["description"] = stage.get("description")
|
|
@@ -213,6 +225,11 @@ def stage_declarations(recipes: Any) -> dict[str, dict[str, Any]]:
|
|
|
213
225
|
recipe_url = getattr(source, "url", None) if source is not None else None
|
|
214
226
|
if recipe_url:
|
|
215
227
|
entry["recipe_url"] = str(recipe_url)
|
|
228
|
+
tool = stage.get("tool")
|
|
229
|
+
if isinstance(tool, dict) and tool.get("name"):
|
|
230
|
+
entry["tool"] = str(tool["name"])
|
|
231
|
+
if stage.get("role"):
|
|
232
|
+
entry["role"] = str(stage["role"])
|
|
216
233
|
for param in stage.get("parameters") or []:
|
|
217
234
|
param_name = param.get("name")
|
|
218
235
|
if not param_name:
|
|
@@ -1027,6 +1044,8 @@ class ProcessedTarget:
|
|
|
1027
1044
|
excluded=bool(entry.get("excluded", False)),
|
|
1028
1045
|
parameters=parameters,
|
|
1029
1046
|
recipe_url=declared.get("recipe_url"),
|
|
1047
|
+
tool=declared.get("tool"),
|
|
1048
|
+
role=declared.get("role"),
|
|
1030
1049
|
)
|
|
1031
1050
|
)
|
|
1032
1051
|
return stages
|
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import html
|
|
5
6
|
import re
|
|
6
7
|
import shutil
|
|
7
8
|
import warnings
|
|
9
|
+
from collections.abc import Sequence
|
|
8
10
|
from importlib import resources
|
|
9
11
|
from pathlib import Path
|
|
10
12
|
from typing import Any
|
|
@@ -16,7 +18,7 @@ from tomlkit.exceptions import ParseError
|
|
|
16
18
|
|
|
17
19
|
from starbash import console
|
|
18
20
|
from starbash.paths import get_publish_site_dir
|
|
19
|
-
from starbash.processed_target import ProcessedTarget
|
|
21
|
+
from starbash.processed_target import ProcessedTarget, StageOption, stage_declarations
|
|
20
22
|
|
|
21
23
|
|
|
22
24
|
def slugify(value: str) -> str:
|
|
@@ -63,6 +65,123 @@ def equipment_rows(equipment: Any) -> list[dict[str, str | None]]:
|
|
|
63
65
|
return rows
|
|
64
66
|
|
|
65
67
|
|
|
68
|
+
#: Stylesheet for :func:`stage_tree_html`, scoped to its wrapper so it cannot
|
|
69
|
+
#: fight the site theme. Colours are tuned for the dark ``jekyll-theme-midnight``
|
|
70
|
+
#: look: defaults are a muted grey-blue, overrides a bold amber accent.
|
|
71
|
+
_STAGE_TREE_CSS = """\
|
|
72
|
+
.sb-stages{margin:1.25em 0;font-size:.92rem;line-height:1.5}
|
|
73
|
+
.sb-stages ul{list-style:none;margin:.25rem 0;padding:0}
|
|
74
|
+
.sb-stages .sb-stage{margin:.35rem 0;padding-left:1.1rem;border-left:2px solid rgba(128,150,180,.45)}
|
|
75
|
+
.sb-stages .sb-stage-name{font-weight:600}
|
|
76
|
+
.sb-stages a.sb-stage-name{color:inherit;text-decoration:underline dotted;text-underline-offset:.15em}
|
|
77
|
+
.sb-stages .sb-tool,.sb-stages .sb-role,.sb-stages .sb-skip{display:inline-block;margin-left:.45rem;padding:.05rem .45rem;border:1px solid rgba(128,150,180,.55);border-radius:.7rem;font-size:.7rem;letter-spacing:.05em;text-transform:uppercase;opacity:.85;vertical-align:.1em}
|
|
78
|
+
.sb-stages .sb-skip{border-style:dashed}
|
|
79
|
+
.sb-stages .sb-stage.excluded{opacity:.55}
|
|
80
|
+
.sb-stages .sb-stage.excluded .sb-stage-name{text-decoration:line-through}
|
|
81
|
+
.sb-stages .sb-recipe{margin-left:.45rem;font-size:.78rem;opacity:.9}
|
|
82
|
+
.sb-stages .sb-stage-desc{margin-left:.45rem;opacity:.7;font-size:.85rem}
|
|
83
|
+
.sb-stages .sb-params{padding-left:1.35rem;margin:.1rem 0}
|
|
84
|
+
.sb-stages .sb-param{margin:.05rem 0}
|
|
85
|
+
.sb-stages .sb-pname{opacity:.92}
|
|
86
|
+
.sb-stages .sb-default{color:#9fb0c3;font-weight:400}
|
|
87
|
+
.sb-stages .sb-override{color:#ffb454;font-weight:700}
|
|
88
|
+
.sb-stages .sb-legend{font-size:.8rem;opacity:.85;margin:.2rem 0 .6rem}"""
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _display_value(value: Any) -> str:
|
|
92
|
+
"""Render a parameter value the way its TOML line would be written."""
|
|
93
|
+
if value is None:
|
|
94
|
+
return ""
|
|
95
|
+
if isinstance(value, bool):
|
|
96
|
+
return "true" if value else "false"
|
|
97
|
+
if isinstance(value, str):
|
|
98
|
+
return f'"{value}"'
|
|
99
|
+
return str(value)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _esc(value: Any) -> str:
|
|
103
|
+
"""HTML-escape any value bound for the stage tree."""
|
|
104
|
+
return html.escape(str(value), quote=True)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _param_row(param: Any) -> str:
|
|
108
|
+
"""One parameter line: grey default, or a bold amber override beside it."""
|
|
109
|
+
bits = ['<li class="sb-param"']
|
|
110
|
+
if param.description:
|
|
111
|
+
bits.append(f' title="{_esc(param.description)}"')
|
|
112
|
+
bits.append(f'><span class="sb-pname">{_esc(param.name)}</span>')
|
|
113
|
+
if param.is_overridden:
|
|
114
|
+
bits.append(f' <span class="sb-override">= {_esc(_display_value(param.value))}</span>')
|
|
115
|
+
if param.default is not None:
|
|
116
|
+
bits.append(
|
|
117
|
+
f' <span class="sb-default">(default {_esc(_display_value(param.default))})</span>'
|
|
118
|
+
)
|
|
119
|
+
elif param.default is not None:
|
|
120
|
+
bits.append(f' <span class="sb-default">= {_esc(_display_value(param.default))}</span>')
|
|
121
|
+
else:
|
|
122
|
+
bits.append(' <span class="sb-default">(no default)</span>')
|
|
123
|
+
bits.append("</li>")
|
|
124
|
+
return "".join(bits)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _stage_row(stage: StageOption) -> list[str]:
|
|
128
|
+
"""The ``<li>`` for one stage: header line plus its parameter lines."""
|
|
129
|
+
css = "sb-stage excluded" if stage.excluded else "sb-stage"
|
|
130
|
+
# The stage name links to the recipe its TOML came from, when that recipe
|
|
131
|
+
# has a browsable (http(s)) URL; local/`pkg://` sources stay plain text.
|
|
132
|
+
recipe_url = str(stage.recipe_url) if stage.recipe_url else ""
|
|
133
|
+
if recipe_url.startswith("http"):
|
|
134
|
+
name_html = f'<a class="sb-stage-name" href="{_esc(recipe_url)}">{_esc(stage.name)}</a>'
|
|
135
|
+
else:
|
|
136
|
+
name_html = f'<span class="sb-stage-name">{_esc(stage.name)}</span>'
|
|
137
|
+
head = [name_html]
|
|
138
|
+
if stage.tool:
|
|
139
|
+
head.append(f'<span class="sb-tool">{_esc(stage.tool)}</span>')
|
|
140
|
+
if stage.role:
|
|
141
|
+
head.append(f'<span class="sb-role">{_esc(stage.role)}</span>')
|
|
142
|
+
if stage.excluded:
|
|
143
|
+
head.append('<span class="sb-skip">skipped</span>')
|
|
144
|
+
if stage.description:
|
|
145
|
+
head.append(f'<span class="sb-stage-desc">{_esc(stage.description)}</span>')
|
|
146
|
+
|
|
147
|
+
lines = [f'<li class="{css}">', f'<div class="sb-stage-head">{" ".join(head)}</div>']
|
|
148
|
+
if stage.parameters:
|
|
149
|
+
lines.append('<ul class="sb-params">')
|
|
150
|
+
lines.extend(_param_row(param) for param in stage.parameters)
|
|
151
|
+
lines.append("</ul>")
|
|
152
|
+
lines.append("</li>")
|
|
153
|
+
return lines
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def stage_tree_html(stages: Sequence[StageOption]) -> str:
|
|
157
|
+
"""Render a target's stages as an HTML tree for its report page.
|
|
158
|
+
|
|
159
|
+
Every stage the target records is shown, in order, with the parameters its
|
|
160
|
+
recipe declares: the recipe *defaults* render in muted grey, while values
|
|
161
|
+
this target *overrides* render in a bolder amber accent right beside the
|
|
162
|
+
default they replaced. Excluded stages stay visible but dimmed and marked
|
|
163
|
+
"skipped", so the tree shows exactly which stages were used.
|
|
164
|
+
|
|
165
|
+
Returns ``""`` for a target with no recorded stages.
|
|
166
|
+
"""
|
|
167
|
+
if not stages:
|
|
168
|
+
return ""
|
|
169
|
+
|
|
170
|
+
lines = [
|
|
171
|
+
'<div class="sb-stages">',
|
|
172
|
+
"<style>",
|
|
173
|
+
_STAGE_TREE_CSS,
|
|
174
|
+
"</style>",
|
|
175
|
+
'<div class="sb-legend">Parameters: <span class="sb-default">grey = recipe default</span>'
|
|
176
|
+
' · <span class="sb-override">amber = this target’s override</span></div>',
|
|
177
|
+
'<ul class="sb-stage-list">',
|
|
178
|
+
]
|
|
179
|
+
for stage in stages:
|
|
180
|
+
lines.extend(_stage_row(stage))
|
|
181
|
+
lines.extend(["</ul>", "</div>"])
|
|
182
|
+
return "\n".join(lines)
|
|
183
|
+
|
|
184
|
+
|
|
66
185
|
class GitHubPublisher:
|
|
67
186
|
"""Generate a complete local Jekyll site from one processed repository."""
|
|
68
187
|
|
|
@@ -103,8 +222,26 @@ class GitHubPublisher:
|
|
|
103
222
|
heroes = [path for path in images if path.name.lower().startswith("hero")]
|
|
104
223
|
return heroes or images
|
|
105
224
|
|
|
106
|
-
def
|
|
107
|
-
|
|
225
|
+
def _stage_declarations(self) -> dict[str, dict[str, Any]]:
|
|
226
|
+
"""Map stage name -> declared description/parameters from the recipe repos.
|
|
227
|
+
|
|
228
|
+
Best effort: contexts without ``get_recipes`` (bare front ends, tests)
|
|
229
|
+
get an empty map, and the report then simply omits recipe-declared
|
|
230
|
+
defaults while still showing the target's recorded stages.
|
|
231
|
+
"""
|
|
232
|
+
get_recipes = getattr(self.sb, "get_recipes", None)
|
|
233
|
+
if not callable(get_recipes):
|
|
234
|
+
return {}
|
|
235
|
+
try:
|
|
236
|
+
return stage_declarations(get_recipes())
|
|
237
|
+
except Exception: # noqa: BLE001 - a broken recipe repo must not kill the report
|
|
238
|
+
warnings.warn(
|
|
239
|
+
"Could not read recipe declarations; the report omits defaults", stacklevel=2
|
|
240
|
+
)
|
|
241
|
+
return {}
|
|
242
|
+
|
|
243
|
+
def _targets(self, root: Path) -> list[tuple[Path, dict[str, Any], ProcessedTarget]]:
|
|
244
|
+
targets: list[tuple[Path, dict[str, Any], ProcessedTarget]] = []
|
|
108
245
|
for directory in sorted(
|
|
109
246
|
(path for path in root.iterdir() if path.is_dir()),
|
|
110
247
|
key=lambda path: path.name.lower(),
|
|
@@ -126,7 +263,7 @@ class GitHubPublisher:
|
|
|
126
263
|
document.update(plain(target.about))
|
|
127
264
|
document.update(plain(target.sessions))
|
|
128
265
|
document["_main_config"] = main_config
|
|
129
|
-
targets.append((directory, document))
|
|
266
|
+
targets.append((directory, document, target))
|
|
130
267
|
except (OSError, ParseError) as exc:
|
|
131
268
|
warnings.warn(f"Skipping malformed target {metadata_dir}: {exc}", stacklevel=2)
|
|
132
269
|
return targets
|
|
@@ -135,6 +272,7 @@ class GitHubPublisher:
|
|
|
135
272
|
"""Regenerate the complete site and return its root directory."""
|
|
136
273
|
root = self._processed_root()
|
|
137
274
|
targets = self._targets(root)
|
|
275
|
+
declarations = self._stage_declarations()
|
|
138
276
|
|
|
139
277
|
# Wipe any previously generated site so stale files don't persist.
|
|
140
278
|
if self.site_dir.exists():
|
|
@@ -178,7 +316,7 @@ class GitHubPublisher:
|
|
|
178
316
|
shutil.copyfileobj(source, destination)
|
|
179
317
|
progress.update(task, description="Copied static assets", advance=1)
|
|
180
318
|
index_targets: list[dict[str, Any]] = []
|
|
181
|
-
for directory, document in targets:
|
|
319
|
+
for directory, document, processed in targets:
|
|
182
320
|
about = document.get("about", {})
|
|
183
321
|
if not isinstance(about, dict):
|
|
184
322
|
about = {}
|
|
@@ -231,6 +369,7 @@ class GitHubPublisher:
|
|
|
231
369
|
)
|
|
232
370
|
page_images = [f"../../{url}" for url in image_urls]
|
|
233
371
|
seo_image = f"/{image_urls[0]}" if image_urls else None
|
|
372
|
+
stage_tree = stage_tree_html(processed.stage_options(declarations))
|
|
234
373
|
page_name = f"{slug}.md"
|
|
235
374
|
post = self.environment.get_template("target.md.jinja").render(
|
|
236
375
|
target={**target, "name": name},
|
|
@@ -240,6 +379,7 @@ class GitHubPublisher:
|
|
|
240
379
|
images=page_images,
|
|
241
380
|
image=seo_image,
|
|
242
381
|
sessions=sessions,
|
|
382
|
+
stage_tree=stage_tree,
|
|
243
383
|
workflow_url=f"../../assets/targets/{slug}/main.toml",
|
|
244
384
|
)
|
|
245
385
|
(posts / page_name).write_text(post)
|
|
@@ -407,7 +407,7 @@ class GitHubService:
|
|
|
407
407
|
{
|
|
408
408
|
"name": name,
|
|
409
409
|
# note: markdown is not supported in the description field
|
|
410
|
-
"description": "
|
|
410
|
+
"description": "(mostly) automatic (beta) image workflows by Starbash",
|
|
411
411
|
"private": False,
|
|
412
412
|
# we want to make main ourselves
|
|
413
413
|
"auto_init": False,
|
|
@@ -9,10 +9,30 @@ description: {{ description | default('Processed Starbash target.', true) | tojs
|
|
|
9
9
|
|
|
10
10
|
{{ about.summary | default('Processed astrophotography target.') }}
|
|
11
11
|
|
|
12
|
+
{% if target.ra is defined and target.ra and target.ra != 'N/A' and target.dec is defined and target.dec and target.dec != 'N/A' %}
|
|
13
|
+
|
|
14
|
+
**Coordinates:** RA {{ target.ra }} / Dec {{ target.dec }}
|
|
15
|
+
|
|
16
|
+
{% endif %}
|
|
17
|
+
|
|
12
18
|
{% for image in images %}
|
|
13
19
|

|
|
14
20
|
{% endfor %}
|
|
15
21
|
|
|
22
|
+
## Workflow
|
|
23
|
+
|
|
24
|
+
{% if stage_tree %}
|
|
25
|
+
|
|
26
|
+
The stages used to process this target, with each stage's parameters:
|
|
27
|
+
|
|
28
|
+
{{ stage_tree }}
|
|
29
|
+
|
|
30
|
+
{% endif %}
|
|
31
|
+
|
|
32
|
+
{% if workflow_url %}
|
|
33
|
+
[View processing workflow]({{ workflow_url }})
|
|
34
|
+
{% endif %}
|
|
35
|
+
|
|
16
36
|
## Sessions
|
|
17
37
|
|
|
18
38
|
{% for session in sessions %}
|
|
@@ -35,8 +55,3 @@ description: {{ description | default('Processed Starbash target.', true) | tojs
|
|
|
35
55
|
|
|
36
56
|
{% endfor %}
|
|
37
57
|
|
|
38
|
-
## Workflow
|
|
39
|
-
|
|
40
|
-
{% if workflow_url %}
|
|
41
|
-
[View processing workflow]({{ workflow_url }})
|
|
42
|
-
{% endif %}
|
|
@@ -61,6 +61,9 @@ class MainWindow(QMainWindow):
|
|
|
61
61
|
super().__init__(parent)
|
|
62
62
|
self.setWindowTitle("Starbash")
|
|
63
63
|
self.resize(1280, 820)
|
|
64
|
+
# A stable handle for the gui-integration movie (doc/plans/gui-integration-video.md
|
|
65
|
+
# §6): a name, not a behaviour - nothing is styled by it.
|
|
66
|
+
self.setObjectName("MainWindow")
|
|
64
67
|
|
|
65
68
|
self._bus = EventBusBridge(self)
|
|
66
69
|
self._sb = sb
|
|
@@ -150,6 +150,7 @@ class ProcessingPage(Page):
|
|
|
150
150
|
layout.addWidget(self._caption)
|
|
151
151
|
|
|
152
152
|
self._tasks = _RunTree()
|
|
153
|
+
self._tasks.setObjectName("processingTree")
|
|
153
154
|
self._tasks.setHeaderLabels(["Target / stage / task", "Status / details"])
|
|
154
155
|
self._tasks.setAlternatingRowColors(True)
|
|
155
156
|
layout.addWidget(self._tasks, 1)
|
|
@@ -160,6 +161,29 @@ class ProcessingPage(Page):
|
|
|
160
161
|
if self.bus is not None:
|
|
161
162
|
self.bus.received.connect(self._on_event) # type: ignore[attr-defined]
|
|
162
163
|
|
|
164
|
+
# --- public handles ---------------------------------------------------
|
|
165
|
+
@property
|
|
166
|
+
def run_button(self) -> QPushButton:
|
|
167
|
+
"""The *Run auto pipeline* button — the movie script's handle for it.
|
|
168
|
+
|
|
169
|
+
It cannot carry an ``objectName`` of its own (it is ``#Primary``, and a widget
|
|
170
|
+
has one ``objectName``), so it is reached through this accessor instead — see
|
|
171
|
+
``doc/plans/gui-integration-video.md`` §6.
|
|
172
|
+
"""
|
|
173
|
+
return self._run
|
|
174
|
+
|
|
175
|
+
@property
|
|
176
|
+
def cancel_button(self) -> QPushButton:
|
|
177
|
+
"""The *Cancel* button beside the run button — same §6 workaround as the
|
|
178
|
+
run button (no ``objectName`` of its own)."""
|
|
179
|
+
return self._cancel
|
|
180
|
+
|
|
181
|
+
@property
|
|
182
|
+
def caption_label(self) -> QLabel:
|
|
183
|
+
"""The one-line run caption (e.g. ``Idle.``) — same §6 workaround as above:
|
|
184
|
+
it is ``#PageSubtitle``, so its ``objectName`` is spoken for."""
|
|
185
|
+
return self._caption
|
|
186
|
+
|
|
163
187
|
# --- actions ----------------------------------------------------------
|
|
164
188
|
def start_run(self) -> None:
|
|
165
189
|
"""Clear the last run's view and start processing in the background.
|
|
@@ -274,12 +274,18 @@ class YouPage(SetupPage):
|
|
|
274
274
|
|
|
275
275
|
repo = sb.user_repo
|
|
276
276
|
self._name = QLineEdit(str(repo.get("user.name", "") or ""))
|
|
277
|
+
# objectName is the gui-integration script's handle (doc/plans/gui-integration-video.md
|
|
278
|
+
# §6). These four are named, never restyled - the theme styles by type here.
|
|
279
|
+
self._name.setObjectName("wizardName")
|
|
277
280
|
self._email = QLineEdit(str(repo.get("user.email", "") or ""))
|
|
281
|
+
self._email.setObjectName("wizardEmail")
|
|
278
282
|
self._analytics = QCheckBox("Send anonymous crash reports and usage data (please!)")
|
|
283
|
+
self._analytics.setObjectName("wizardAnalytics")
|
|
279
284
|
self._analytics.setChecked(analytics_enabled(repo))
|
|
280
285
|
self._include_email = QCheckBox(
|
|
281
286
|
"Include my email with crash reports (so we can contact you)"
|
|
282
287
|
)
|
|
288
|
+
self._include_email.setObjectName("wizardIncludeEmail")
|
|
283
289
|
self._include_email.setChecked(analytics_include_user(repo))
|
|
284
290
|
|
|
285
291
|
# The *include email* option only means something once an email is typed.
|
|
@@ -548,6 +554,17 @@ class ImagesPage(SetupPage):
|
|
|
548
554
|
|
|
549
555
|
self.refresh()
|
|
550
556
|
|
|
557
|
+
@property
|
|
558
|
+
def choose_button(self) -> QPushButton:
|
|
559
|
+
"""The *Choose folder…* button — the movie script's handle for it.
|
|
560
|
+
|
|
561
|
+
It cannot carry an ``objectName`` of its own: it is ``#Primary`` (the theme's
|
|
562
|
+
accent style) and a widget has only one ``objectName``. A public accessor is
|
|
563
|
+
the workaround the plan settled on — see ``doc/plans/gui-integration-video.md``
|
|
564
|
+
§6.
|
|
565
|
+
"""
|
|
566
|
+
return self._choose
|
|
567
|
+
|
|
551
568
|
def _known_paths(self) -> set[str]:
|
|
552
569
|
"""The filesystem paths of the raw-image repos we already know about."""
|
|
553
570
|
known: set[str] = set()
|
|
@@ -869,6 +886,9 @@ class SetupWizard(QWizard):
|
|
|
869
886
|
self.action: str | None = None
|
|
870
887
|
|
|
871
888
|
self.setWindowTitle("Starbash setup")
|
|
889
|
+
# A stable handle for the gui-integration movie (doc/plans/gui-integration-video.md
|
|
890
|
+
# §6); names, not behaviour.
|
|
891
|
+
self.setObjectName("SetupWizard")
|
|
872
892
|
self.setWizardStyle(QWizard.WizardStyle.ModernStyle)
|
|
873
893
|
# Page 1 is the start of the story, so a greyed-out *Back* is just noise.
|
|
874
894
|
self.setOption(QWizard.WizardOption.NoBackButtonOnStartPage)
|
|
@@ -901,6 +921,11 @@ class SetupWizard(QWizard):
|
|
|
901
921
|
self.setOption(QWizard.WizardOption.HaveCustomButton2)
|
|
902
922
|
self.setButtonText(QWizard.WizardButton.CustomButton1, "Process all my targets")
|
|
903
923
|
self.setButtonText(QWizard.WizardButton.CustomButton2, "Pick a target to process")
|
|
924
|
+
# The script's handles for those two actions (doc/plans/gui-integration-video.md
|
|
925
|
+
# §6). Qt pre-created the buttons, so naming them here is enough - and the
|
|
926
|
+
# names are new ids, so the theme (which styles nothing of theirs) is untouched.
|
|
927
|
+
self._name_action_button(QWizard.WizardButton.CustomButton1, "wizardProcessAllTargets")
|
|
928
|
+
self._name_action_button(QWizard.WizardButton.CustomButton2, "wizardPickTarget")
|
|
904
929
|
# Qt creates those two *enabled* and shows them on every page, and it never
|
|
905
930
|
# consults isComplete() for custom buttons - so without this the wizard
|
|
906
931
|
# offered a live "Process all my targets" on page 1 and only switched it off
|
|
@@ -959,6 +984,16 @@ class SetupWizard(QWizard):
|
|
|
959
984
|
buttons.append(button)
|
|
960
985
|
return buttons
|
|
961
986
|
|
|
987
|
+
def _name_action_button(self, which: QWizard.WizardButton, name: str) -> None:
|
|
988
|
+
"""Give one of Qt's custom buttons an ``objectName`` (see the constructor).
|
|
989
|
+
|
|
990
|
+
Qt types :meth:`button` as ``QAbstractButton``; the closing actions are the
|
|
991
|
+
``QPushButton`` it actually created.
|
|
992
|
+
"""
|
|
993
|
+
button = self.button(which)
|
|
994
|
+
if isinstance(button, QPushButton):
|
|
995
|
+
button.setObjectName(name)
|
|
996
|
+
|
|
962
997
|
def _disable_action_buttons(self) -> None:
|
|
963
998
|
"""Switch the closing actions off, and say when they come alive.
|
|
964
999
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|