11"""Basic tutorial structure."""
22
3+ import os
34from pathlib import Path
45from typing import Annotated , Any , Literal , Self
56
@@ -34,6 +35,12 @@ class TestSpecificationMixin:
3435 backoff_factor : PositiveFloat = 0 # {backoff factor} * (2 ** ({number of previous retries}))
3536
3637
38+ class ConfigurationMixin :
39+ """Mixin for configuration models."""
40+
41+ skip : bool = False
42+
43+
3744class CleanupCommandModel (CommandBaseModel ):
3845 """Model for cleanup commands."""
3946
@@ -59,7 +66,7 @@ class TestPortModel(TestSpecificationMixin, BaseModel):
5966 port : Annotated [int , Field (ge = 0 , le = 65535 )]
6067
6168
62- class CommandRuntimeConfigurationModel (CommandBaseModel ):
69+ class CommandRuntimeConfigurationModel (ConfigurationMixin , CommandBaseModel ):
6370 """Model for runtime configuration when running a single command."""
6471
6572 model_config = ConfigDict (extra = "forbid" )
@@ -69,7 +76,7 @@ class CommandRuntimeConfigurationModel(CommandBaseModel):
6976 test : tuple [TestCommandModel | TestPortModel , ...] = tuple ()
7077
7178
72- class CommandDocumentationConfigurationModel (BaseModel ):
79+ class CommandDocumentationConfigurationModel (ConfigurationMixin , BaseModel ):
7380 """Model for documenting a single command."""
7481
7582 model_config = ConfigDict (extra = "forbid" )
@@ -88,6 +95,18 @@ class CommandModel(BaseModel):
8895 doc : CommandDocumentationConfigurationModel = CommandDocumentationConfigurationModel ()
8996
9097
98+ class CommandsRuntimeConfigurationModel (ConfigurationMixin , BaseModel ):
99+ """Runtime configuration for an entire commands part."""
100+
101+ model_config = ConfigDict (extra = "forbid" )
102+
103+
104+ class CommandsDocumentationConfigurationModel (ConfigurationMixin , BaseModel ):
105+ """Documentation configuration for an entire commands part."""
106+
107+ model_config = ConfigDict (extra = "forbid" )
108+
109+
91110class CommandsPartModel (BaseModel ):
92111 """Model for a set of commands."""
93112
@@ -96,11 +115,24 @@ class CommandsPartModel(BaseModel):
96115 type : Literal ["commands" ] = "commands"
97116 commands : tuple [CommandModel , ...]
98117
118+ run : CommandsRuntimeConfigurationModel = CommandsRuntimeConfigurationModel ()
119+ doc : CommandsDocumentationConfigurationModel = CommandsDocumentationConfigurationModel ()
120+
121+
122+ class FileRuntimeConfigurationModel (ConfigurationMixin , BaseModel ):
123+ """Runtime configuration for a file part."""
124+
125+ model_config = ConfigDict (extra = "forbid" )
126+
127+
128+ class FileDocumentationConfigurationModel (ConfigurationMixin , BaseModel ):
129+ """Documentation configuration for a file part."""
130+
131+ model_config = ConfigDict (extra = "forbid" )
99132
100- class FileDocumentationConfigurationModel (BaseModel ):
101133 # sphinx options:
102134 language : str = ""
103- caption : str | Literal [False ] = ""
135+ caption : str | Literal [False ] = "" # template
104136 linenos : bool = False
105137 lineno_start : PositiveInt | Literal [False ] = False
106138 emphasize_lines : str = ""
@@ -113,12 +145,13 @@ class FilePartModel(BaseModel):
113145 model_config = ConfigDict (extra = "forbid" )
114146
115147 type : Literal ["file" ] = "file"
116- contents : str | None = None
117- source : Path | None = None
118- destination : Path
148+ contents : str | None = None # template if property is set
149+ source : Path | None = None # template if property is set
150+ destination : str # template
119151 template : bool = True
120152
121153 doc : FileDocumentationConfigurationModel = FileDocumentationConfigurationModel ()
154+ run : FileRuntimeConfigurationModel = FileRuntimeConfigurationModel ()
122155
123156 @field_validator ("source" , mode = "after" )
124157 @classmethod
@@ -135,6 +168,12 @@ def validate_contents_or_source(self) -> Self:
135168 raise ValueError ("Only one of contents or source is allowed." )
136169 return self
137170
171+ @model_validator (mode = "after" )
172+ def validate_destination (self ) -> Self :
173+ if not self .source and self .destination .endswith (os .sep ):
174+ raise ValueError (f"{ self .destination } : Destination must not be a directory if contents is given." )
175+ return self
176+
138177
139178class RuntimeConfigurationModel (BaseModel ):
140179 """Model for configuration at runtime."""
0 commit comments