Skip to content

Commit d99891a

Browse files
committed
feat(model)
- incremental improvements to models (adding examples)
1 parent 45aab73 commit d99891a

File tree

11 files changed

+293
-78
lines changed

11 files changed

+293
-78
lines changed

Makefile

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,12 +142,16 @@ run-worker: run-compose
142142
alembic upgrade head
143143
python3 -m lsst.cmservice.daemon
144144

145+
.PHONY: docs
146+
docs:
147+
uv run script/render_jsonschema.py --clean --html
148+
145149
.PHONY: packages
146150
packages:
147151
$(MAKE) -C packages/cm-canvas rebuild
148152

149153
.PHONY: run-web
150-
run-web: $(PY_VENV) packages
154+
run-web: $(PY_VENV) docs packages
151155
uv run web
152156

153157
.PHONY: migrate

packages/cm-web/src/lsst/cmservice/web/components/dialog.py

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ def dialog_content(self, title: str) -> None:
211211
self.ribbon()
212212

213213
with ui.splitter(limits=(50, 100), value=100).classes(
214-
"w-full max-w-full min-h-[4rem]"
214+
"w-full h-full max-w-full min-h-[4rem]"
215215
) as self.splitter:
216216
with self.splitter.before:
217217
self.editor_section()
@@ -287,12 +287,13 @@ def editor_section(self) -> None:
287287

288288
@ui.refreshable_method
289289
def help_section(self) -> None:
290-
if self.context.kind is None:
291-
ui.label("No help available.")
292-
return None
293-
ui.element("iframe").props(f"src='/static/docs/{self.context.kind}_spec.html'").classes(
294-
"w-full h-full"
295-
)
290+
with ui.element("div").classes("w-full h-full"):
291+
if self.context.kind is None:
292+
ui.label("No help available.")
293+
return None
294+
ui.element("iframe").props(f"src='/static/docs/{self.context.kind}_spec.html'").classes(
295+
"w-full h-full"
296+
)
296297

297298
@ui.refreshable_method
298299
def action_section(self) -> None:
@@ -619,12 +620,12 @@ def handle_apply_template(self) -> None:
619620
template = {}
620621
kind_model = KIND_TO_SPEC[self.context.kind]
621622
for field_name, field_info in kind_model.model_fields.items():
622-
if field_info.default_factory is not None:
623-
template[field_name] = field_info.default_factory() # type: ignore[call-arg]
623+
if field_info.examples:
624+
template[field_name] = field_info.examples[0]
624625
elif field_info.default is not PydanticUndefined:
625626
template[field_name] = field_info.default
626-
elif field_info.examples:
627-
template[field_name] = field_info.examples[0]
627+
elif field_info.default_factory is not None:
628+
template[field_name] = field_info.default_factory() # type: ignore[call-arg]
628629
else:
629630
template[field_name] = None
630631
self.editor.set_value(yaml.safe_dump(template))

script/render_jsonschema.py

100644100755
Lines changed: 53 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,94 @@
1+
#!/usr/bin/env -S uv run --script
2+
# /// script
3+
# requires-python = ">=3.13"
4+
# dependencies = [
5+
# "click==8.1.*",
6+
# "json-schema-for-humans>=1.5.1",
7+
# "lsst-cm-service",
8+
# "lsst-utils",
9+
# "pydantic==2.11.*",
10+
# ]
11+
#
12+
# [tool.uv.sources]
13+
# lsst-cm-service = { path = "../" }
14+
# ///
115
"""Loads the CM Service Manifest Models and generates JSONSchema files"""
216

317
import json
18+
import shutil
419
from pathlib import Path
520

21+
import click
622
from json_schema_for_humans.generate import generate_from_file_object
723
from json_schema_for_humans.generation_configuration import GenerationConfiguration
24+
from pydantic import BaseModel
825

926
from lsst.cmservice.models.manifests import bps, butler, facility, lsst, steps, wms
1027

1128
GENCONFIG = GenerationConfiguration(
1229
template_name="js_offline",
13-
show_breadcrumbs=True,
30+
show_breadcrumbs=False,
31+
examples_as_yaml=True,
1432
collapse_long_descriptions=False,
1533
link_to_reused_ref=True,
1634
description_is_markdown=True,
35+
with_footer=False,
1736
)
1837

1938
MDCONFIG = GenerationConfiguration(
2039
template_name="md",
21-
show_breadcrumbs=True,
40+
show_breadcrumbs=False,
2241
collapse_long_descriptions=False,
2342
link_to_reused_ref=True,
2443
description_is_markdown=True,
2544
)
2645

27-
STATIC_DIR = Path("packages/cm-web/src/lsst/cmservice/web/static/docs")
46+
STATIC_DIR = Path(__file__).parent.parent / "packages/cm-web/src/lsst/cmservice/web/static/docs"
2847

2948

30-
def main():
49+
@click.command()
50+
@click.option("--html", is_flag=True, help="Output HTML")
51+
@click.option("--markdown", is_flag=True, help="Output Markdown")
52+
@click.option("--jsonschema", is_flag=True, default=True, help="Output JSON Schema")
53+
@click.option("--clean/--no-clean", default=False, help="Empty target directory first")
54+
@click.option(
55+
"--output",
56+
default=STATIC_DIR,
57+
help="Output directory",
58+
type=click.Path(exists=False, file_okay=False, dir_okay=True, path_type=Path),
59+
)
60+
def main(*, html: bool, markdown: bool, jsonschema: bool, clean: bool, output: Path) -> None:
61+
if clean and output.exists():
62+
shutil.rmtree(output)
63+
64+
output.mkdir(parents=False, exist_ok=True)
65+
66+
manifest: BaseModel
3167
for manifest in [
3268
bps.BpsSpec,
3369
butler.ButlerSpec,
3470
facility.FacilitySpec,
3571
lsst.LsstSpec,
72+
steps.BreakpointSpec,
3673
steps.StepSpec,
3774
wms.WmsSpec,
3875
]:
39-
STATIC_DIR.mkdir(parents=False, exist_ok=True)
4076
manifest_schema = manifest.model_json_schema()
4177
manifest_title = manifest_schema["title"]
42-
schema_file = STATIC_DIR / f"{manifest_title}.jsonschema"
43-
human_file = schema_file.with_suffix(".html")
44-
markdown_file = schema_file.with_suffix(".md")
45-
schema_file.write_text(json.dumps(manifest_schema, indent=2))
4678

47-
with schema_file.open("r") as in_, human_file.open("w") as out_:
48-
generate_from_file_object(in_, out_, config=GENCONFIG)
79+
if jsonschema:
80+
schema_file = output / f"{manifest_title}.jsonschema"
81+
schema_file.write_text(json.dumps(manifest_schema, indent=2))
82+
83+
if html:
84+
human_file = schema_file.with_suffix(".html")
85+
with schema_file.open("r") as in_, human_file.open("w") as out_:
86+
generate_from_file_object(in_, out_, config=GENCONFIG)
4987

50-
with schema_file.open("r") as in_, markdown_file.open("w") as out_:
51-
generate_from_file_object(in_, out_, config=MDCONFIG)
88+
if markdown:
89+
markdown_file = schema_file.with_suffix(".md")
90+
with schema_file.open("r") as in_, markdown_file.open("w") as out_:
91+
generate_from_file_object(in_, out_, config=MDCONFIG)
5292

5393

5494
if __name__ == "__main__":

src/lsst/cmservice/models/manifests/bps.py

Lines changed: 48 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@
1212

1313

1414
class BpsSpec(ManifestSpec):
15-
"""Model specification for a BPS Manifest configuration document. Only
16-
the first-class attributes of this model will be available when rendering
17-
a campaign template.
15+
"""Configuration specification for a BPS Manifest. This is used primarily
16+
to fulfill a BPS submission file for a Group. These parameters may be set
17+
in a campaign-level BPS manifest or on the Step or Group under a `bps`
18+
mapping.
1819
"""
1920

2021
model_config = SPEC_CONFIG
@@ -25,41 +26,68 @@ class BpsSpec(ManifestSpec):
2526
)
2627
variables: dict[str, str] | None = Field(
2728
default=None,
28-
description="A mapping of name-value string pairs used to define addtional "
29-
"top-level BPS substitution variables. Note that the values are quoted in the "
30-
"output.",
29+
description="A mapping of name-value string pairs used to define addtional top-level BPS settings "
30+
"or substitution variables. Note that the values are quoted in the output. For values that should "
31+
"not be quoted or otherwise used literally, see `literals`",
32+
examples=[{"operator": "lsstsvc1"}],
33+
)
34+
include_files: list[str] | None = Field(
35+
default_factory=list,
36+
description="A list of include files added to the BPS submission file under the `includeConfigs`"
37+
" heading. This list is combined with `include_files` from other manifests.",
38+
examples=[["${CTRL_BPS_DIR}/python/lsst/ctrl/bps/etc/bps_default.yaml"]],
3139
)
32-
include_files: list[str] | None = Field(default=None)
3340
literals: dict[str, Any] | None = Field(
3441
default=None,
35-
description="A mapping of arbitrary top-level mapping sections to be added as additional "
36-
"literal YAML, e.g., `finalJob`.",
42+
description="A mapping of arbitrary key-value sections to be added as additional literal YAML to "
43+
"the BPS submission file. For setting arbitrary BPS substitution variables, use `variables`. ",
44+
examples=[
45+
{
46+
"requestMemory": 2048,
47+
"numberOfRetries": 5,
48+
"retryUnlessExit": [1, 2],
49+
"finalJob": {"command1": "echo HELLO WORLD"},
50+
}
51+
],
3752
)
3853
environment: dict[str, str] | None = Field(
3954
default=None,
40-
description="A mapping of name-value string pairs used to defined additional "
41-
"values under the `environment` heading.",
55+
description="A mapping of name-value string pairs used to defined additional values under the "
56+
"`environment` heading of the BPS submission file.",
57+
min_length=1,
58+
examples=[{"LSST_S3_USE_THREADS": 1}],
4259
)
4360
payload: dict[str, str] | None = Field(
4461
default=None,
45-
description="A mapping of name-value string pairs used to define BPS payload "
46-
"options. Note that these values are generated from other configuration "
47-
"sources at runtime.",
62+
description="A mapping of name-value string pairs used to define BPS payload options. "
63+
"Note that these values are generated from other configuration sources at runtime.",
4864
)
4965
extra_init_options: str | None = Field(
50-
default=None, description="Options added to the end of pipetaskinit"
66+
default=None, description="Passthrough options added to the end of pipetaskinit"
5167
)
5268
extra_qgraph_options: str | None = Field(
53-
default=None, description="Options added to the end of command line when creating a quantumgraph."
69+
default=None, description="Passthrough options for QuantumGraph builder."
5470
)
5571
extra_run_quantum_options: str | None = Field(
56-
default=None, description="Options added to the end of pipetask command to run a quantum"
72+
default=None, description="Passthrough options for Quantum execution", examples=["--no-versions"]
73+
)
74+
extra_update_qgraph_options: str | None = Field(
75+
default=None, description="Passthrough options for QuantumGraph updater."
5776
)
58-
extra_update_qgraph_options: str | None = Field(default=None)
5977
clustering: dict[str, Any] | None = Field(
6078
default=None,
61-
description="A mapping of clustering directives, added as literal YAML "
62-
"under the `clustering` heading.",
79+
description="A mapping of labeled clustering directives, added as literal YAML under the `cluster` "
80+
"heading. The top-level `clusterAlgorithm` should be added to `literals`.",
81+
examples=[
82+
{
83+
"clusterLabel1": {
84+
"dimensions": "detector",
85+
"pipetasks": "isr,calibrateImage",
86+
"partitionDimensions": "exposure",
87+
"partititionMaxClusters": 10000,
88+
}
89+
}
90+
],
6391
)
6492
operator: str = Field(
6593
default="cmservice",

src/lsst/cmservice/models/manifests/butler.py

Lines changed: 84 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,38 +20,108 @@ class ButlerCollectionsSpec(BaseModel):
2020

2121
model_config = SPEC_CONFIG
2222
campaign_input: list[str] = Field(
23-
description="The campaign source collection",
23+
description="The campaign source collection. The input collection list should not include environment"
24+
" or BPS variables that will be unknown to the CM Service.",
2425
default_factory=list,
2526
validation_alias=AliasChoices("campaign_input", "in"),
27+
examples=[["LSSTCam/defaults"]],
28+
)
29+
ancillary: list[str] = Field(
30+
default_factory=list,
31+
deprecated=True,
32+
description="A set of collections related to the campaign input collections that will be chained "
33+
"together to create a collection for the campaign.",
34+
examples=[["refcats", "skymaps"]],
2635
)
27-
ancillary: list[str] = Field(default_factory=list)
2836
campaign_public_output: str = Field(
29-
description="The public output collection for this Campaign.",
37+
description="The final 'public' campaign *chained* collection; includes the `campaign_output` "
38+
"collection, the Campaign 'input' collection, and any other incidental collections created during "
39+
"the campaign (e.g., resource usage)",
3040
default_factory=lambda: uuid4().__str__(),
3141
validation_alias=AliasChoices("campaign_public_output", "out"),
42+
examples=["u/{operator}/{campaign}"],
3243
)
3344
campaign_output: str | None = Field(
34-
default=None, description="The private output collection for a campaign; {out}/output"
45+
default=None,
46+
description="The 'private' output collection for a campaign; a *chained* collection of "
47+
"each step-specific *output* collection, which itself is a *chained* collection of each step-group's "
48+
"`run` collection.",
49+
examples=["u/{operator}/{campaign}/out"],
50+
)
51+
step_input: str | None = Field(
52+
default=None,
53+
description="The *chained* input collection for a step, usually consisting of the campaign input and "
54+
"any ancestor `step_output` collection."
55+
" This is used internally and generally does not need to be configured.",
56+
examples=["{campaign_public_output}/{step}/input"],
57+
)
58+
step_output: str | None = Field(
59+
default=None,
60+
description="The *chained* output collection for a step, usually consisting of each step-group's "
61+
"`run` collection."
62+
" This is used internally and generally does not need to be configured.",
63+
examples=["{campaign_public_output}/{step}_output"],
64+
)
65+
step_public_output: str | None = Field(
66+
default=None,
67+
description="The *chained* output collection that includes the `step_output` and additional step "
68+
"and/or campaign inputs."
69+
" This is used internally and generally does not need to be configured.",
70+
examples=["{campaign_public_output}/{step}"],
71+
)
72+
group_output: str | None = Field(
73+
default=None,
74+
description="A collection name associated with the `payload.output` BPS setting."
75+
" This is used internally and generally does not need to be configured.",
76+
examples=["{campaign_public_output}/{step}/{group_nonce}", "u/{operator}/{payloadName}"],
3577
)
36-
step_input: str | None = Field(default=None, description="{out}/{step}/input")
37-
step_output: str | None = Field(default=None, description="{out}/{step}_output")
38-
step_public_output: str | None = Field(default=None, description="{out}/{step}")
39-
group_output: str | None = Field(default=None, description="{out}/{step}/{group}")
4078
run: str | None = Field(
4179
default=None,
42-
description="The run collection affected by a Node's execution; {out}/{step}/{group}/{job}",
80+
description="The run collection created by a group's execution (the `payload.outputRun` BPS setting)."
81+
" This is used internally and generally does not need to be configured.",
82+
examples=["{out}/{step}/{group}/{job}"],
4383
validation_alias=AliasChoices("job_run", "run"),
4484
)
4585

4686

4787
class ButlerSpec(ManifestSpec):
48-
"""Spec model for a Butler Manifest."""
88+
"""Configuration specification for a Butler Manifest. This is primarily
89+
used to manage collections and group splitting rules throughout a campaign,
90+
as well as populating the `payload` section of a BPS submission file.
91+
"""
4992

5093
model_config = SPEC_CONFIG
51-
collections: ButlerCollectionsSpec
52-
predicates: Sequence[str] = Field(default_factory=list)
53-
repo: str = Field(description="Name of a Butler known to the application's Butler Factory.")
54-
include_files: list[str] | None = Field(default=None)
94+
collections: ButlerCollectionsSpec = Field(
95+
description="A butler configuration used to specify collections for "
96+
"various campaign operations. Of particular interest for a Butler"
97+
"manifest are `campaign_input` and `campaign_output`",
98+
examples=[
99+
{
100+
"campaign_input": ["LSSTCam/defaults"],
101+
"campaign_public_output": "u/{operator}/{campaign}",
102+
"campaign_output": "u/{operator}/{campaign}/out",
103+
}
104+
],
105+
)
106+
predicates: Sequence[str] = Field(
107+
default_factory=list,
108+
description="A set of data query predicates shared with all users of"
109+
"this manifest. All predicate sets are `AND`-ed together for a final "
110+
"data query",
111+
examples=[
112+
["instrument='LSSTCam'", "skymap='lsst_cells_v2'"],
113+
],
114+
)
115+
repo: str = Field(
116+
description="Name of a Butler known to the application's Butler Factory.",
117+
examples=["/repo/main", "embargo"],
118+
)
119+
include_files: list[str] | None = Field(
120+
default=None,
121+
description="A list of files to be added to the BPS submission as include files "
122+
"that are specific to the use of this Butler.",
123+
examples=["${DRP_PIPE_DIR}/includes/butler/{butler-tuning}.yaml"],
124+
)
55125

56126

57127
class ButlerManifest(LibraryManifest[ButlerSpec]): ...

0 commit comments

Comments
 (0)