dockercomposefile 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,67 @@
1
+ """Network models."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pydantic import Field, field_validator
6
+
7
+ from .common import ComposeBaseModel, _parse_external, _parse_list_or_dict
8
+
9
+
10
+ class IPAMConfig(ComposeBaseModel):
11
+ """IPAM configuration entry."""
12
+
13
+ subnet: str | None = None
14
+ ip_range: str | None = None
15
+ gateway: str | None = None
16
+ aux_addresses: dict[str, str] | None = None
17
+
18
+
19
+ class IPAM(ComposeBaseModel):
20
+ """IP Address Management configuration."""
21
+
22
+ driver: str | None = None
23
+ config: list[IPAMConfig] | None = None
24
+ options: dict[str, str] | None = None
25
+
26
+
27
+ class Network(ComposeBaseModel):
28
+ """Top-level network definition."""
29
+
30
+ driver: str | None = None
31
+ driver_opts: dict[str, str | int] | None = None
32
+ attachable: bool | None = None
33
+ enable_ipv4: bool | None = None
34
+ enable_ipv6: bool | None = None
35
+ ipam: IPAM | None = None
36
+ internal: bool | None = None
37
+ labels: dict[str, str] | list[str] | None = Field(
38
+ default=None, validate_default=True
39
+ )
40
+ external: bool | dict[str, str] | None = Field(
41
+ default=None, validate_default=True
42
+ )
43
+ name: str | None = None
44
+
45
+ @field_validator("labels", mode="before")
46
+ @staticmethod
47
+ def _labels(v):
48
+ return _parse_list_or_dict(v) if v is not None else None
49
+
50
+ @field_validator("external", mode="before")
51
+ @staticmethod
52
+ def _external(v):
53
+ return _parse_external(v) if v is not None else None
54
+
55
+
56
+ class ServiceNetworkConfig(ComposeBaseModel):
57
+ """Service-level network attachment configuration."""
58
+
59
+ aliases: list[str] | None = None
60
+ ipv4_address: str | None = None
61
+ ipv6_address: str | None = None
62
+ link_local_ips: list[str] | None = None
63
+ mac_address: str | None = None
64
+ driver_opts: dict[str, str | int] | None = None
65
+ priority: int | None = None
66
+ gw_priority: int | None = None
67
+ interface_name: str | None = None
@@ -0,0 +1,42 @@
1
+ """Secret models."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pydantic import Field, field_validator
6
+
7
+ from .common import ComposeBaseModel, _parse_external, _parse_list_or_dict
8
+
9
+
10
+ class Secret(ComposeBaseModel):
11
+ """Top-level secret definition."""
12
+
13
+ file: str | None = None
14
+ environment: str | None = None
15
+ external: bool | dict[str, str] | None = Field(
16
+ default=None, validate_default=True
17
+ )
18
+ name: str | None = None
19
+ labels: dict[str, str] | list[str] | None = Field(
20
+ default=None, validate_default=True
21
+ )
22
+ template_driver: str | None = None
23
+
24
+ @field_validator("labels", mode="before")
25
+ @staticmethod
26
+ def _labels(v):
27
+ return _parse_list_or_dict(v) if v is not None else None
28
+
29
+ @field_validator("external", mode="before")
30
+ @staticmethod
31
+ def _external(v):
32
+ return _parse_external(v) if v is not None else None
33
+
34
+
35
+ class ServiceSecret(ComposeBaseModel):
36
+ """Service-level secret reference."""
37
+
38
+ source: str
39
+ target: str | None = None
40
+ uid: str | None = None
41
+ gid: str | None = None
42
+ mode: str | None = None
@@ -0,0 +1,598 @@
1
+ """Service model and sub-models."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from pydantic import Field, field_validator
8
+
9
+ from .build import BuildConfig
10
+ from .config import ServiceConfig
11
+ from .common import (
12
+ ComposeBaseModel,
13
+ _parse_annotations,
14
+ _parse_command,
15
+ _parse_devices,
16
+ _parse_dns,
17
+ _parse_environment,
18
+ _parse_extra_hosts,
19
+ _parse_labels,
20
+ _parse_port,
21
+ _parse_service_volume,
22
+ _parse_tmpfs,
23
+ _parse_ulimits,
24
+ _parse_volumes_from,
25
+ validate_byte_value,
26
+ validate_duration,
27
+ )
28
+ from .deploy import DeployConfig
29
+ from .develop import DevelopConfig
30
+ from .model import ServiceModelConfig
31
+ from .network import ServiceNetworkConfig
32
+ from .secret import ServiceSecret
33
+ from .volume import VolumeMount
34
+
35
+
36
+ class BlkioLimit(ComposeBaseModel):
37
+ """Block IO limit for a specific device."""
38
+
39
+ path: str
40
+ rate: str | int
41
+
42
+
43
+ class BlkioWeight(ComposeBaseModel):
44
+ """Block IO weight for a specific device."""
45
+
46
+ path: str
47
+ weight: int
48
+
49
+
50
+ class BlkioConfig(ComposeBaseModel):
51
+ """Block IO configuration."""
52
+
53
+ weight: int | None = None
54
+ weight_device: list[BlkioWeight] | None = None
55
+ device_read_bps: list[BlkioLimit] | None = None
56
+ device_read_iops: list[BlkioLimit] | None = None
57
+ device_write_bps: list[BlkioLimit] | None = None
58
+ device_write_iops: list[BlkioLimit] | None = None
59
+
60
+
61
+ class Healthcheck(ComposeBaseModel):
62
+ """Healthcheck configuration."""
63
+
64
+ test: str | list[str] | None = None
65
+ interval: str | None = Field(default=None, validate_default=True)
66
+ timeout: str | None = Field(default=None, validate_default=True)
67
+ retries: int | None = None
68
+ start_period: str | None = Field(default=None, validate_default=True)
69
+ start_interval: str | None = Field(default=None, validate_default=True)
70
+ disable: bool | None = None
71
+
72
+ @field_validator("interval", "timeout", "start_period", "start_interval", mode="before")
73
+ @staticmethod
74
+ def _validate_duration(v: Any) -> str | None:
75
+ return validate_duration(v) if v is not None else None
76
+
77
+
78
+ class Logging(ComposeBaseModel):
79
+ """Logging configuration."""
80
+
81
+ driver: str | None = None
82
+ options: dict[str, str] | None = None
83
+
84
+
85
+ class PortConfig(ComposeBaseModel):
86
+ """Port configuration (long syntax)."""
87
+
88
+ target: int
89
+ published: str | None = None
90
+ host_ip: str | None = None
91
+ protocol: str | None = None
92
+ app_protocol: str | None = None
93
+ mode: str | None = None
94
+ name: str | None = None
95
+
96
+
97
+ class Ulimit(ComposeBaseModel):
98
+ """Ulimit configuration."""
99
+
100
+ soft: int
101
+ hard: int
102
+
103
+
104
+ class CredentialSpec(ComposeBaseModel):
105
+ """Credential specification for Windows containers."""
106
+
107
+ file: str | None = None
108
+ registry: str | None = None
109
+ config: str | None = None
110
+
111
+
112
+ class DependsOnConfig(ComposeBaseModel):
113
+ """Long-form depends_on entry."""
114
+
115
+ condition: str | None = None
116
+ restart: bool | None = None
117
+ required: bool | None = None
118
+
119
+
120
+ class ExtendsConfig(ComposeBaseModel):
121
+ """Extends configuration."""
122
+
123
+ service: str
124
+ file: str | None = None
125
+
126
+
127
+ class EnvFileEntry(ComposeBaseModel):
128
+ """Environment file entry with options."""
129
+
130
+ path: str
131
+ required: bool = True
132
+ format: str | None = None
133
+
134
+
135
+ class PostStartHook(ComposeBaseModel):
136
+ """Post-start lifecycle hook."""
137
+
138
+ command: str | list[str]
139
+ user: str | None = None
140
+ privileged: bool | None = None
141
+ working_dir: str | None = None
142
+ environment: dict[str, str] | list[str] | None = None
143
+
144
+
145
+ class ProviderConfig(ComposeBaseModel):
146
+ """Provider configuration for delegated services."""
147
+
148
+ type: str
149
+ options: dict[str, Any] | None = None
150
+
151
+
152
+ class GpuConfig(ComposeBaseModel):
153
+ """GPU configuration."""
154
+
155
+ driver: str | None = None
156
+ count: int | str | None = None
157
+ device_ids: list[str] | None = None
158
+ capabilities: list[str] | None = None
159
+ options: dict[str, Any] | None = None
160
+
161
+
162
+ class Service(ComposeBaseModel):
163
+ """Service definition in a Compose file."""
164
+
165
+ # Core & Image
166
+ image: str | None = None
167
+ build: "BuildConfig | str | None" = Field(default=None, validate_default=True)
168
+ command: str | list[str] | None = Field(default=None, validate_default=True)
169
+ entrypoint: str | list[str] | None = Field(default=None, validate_default=True)
170
+
171
+ # Environment
172
+ environment: dict[str, str | None] | None = Field(
173
+ default=None, validate_default=True
174
+ )
175
+ env_file: list[EnvFileEntry] | list[str] | str | None = Field(
176
+ default=None, validate_default=True
177
+ )
178
+
179
+ # Networking
180
+ ports: list[PortConfig] | list[str] | None = Field(
181
+ default=None, validate_default=True
182
+ )
183
+ expose: list[str] | None = None
184
+ networks: dict[str, "ServiceNetworkConfig"] | list[str] | None = None
185
+ network_mode: str | None = None
186
+ hostname: str | None = None
187
+ domainname: str | None = None
188
+ mac_address: str | None = None
189
+ extra_hosts: dict[str, str] | list[str] | None = Field(
190
+ default=None, validate_default=True
191
+ )
192
+ dns: str | list[str] | None = Field(default=None, validate_default=True)
193
+ dns_search: str | list[str] | None = Field(default=None, validate_default=True)
194
+ dns_opt: list[str] | None = None
195
+ external_links: list[str] | None = None
196
+ links: list[str] | None = None
197
+
198
+ # Storage
199
+ volumes: list["VolumeMount"] | list[str] | None = Field(
200
+ default=None, validate_default=True
201
+ )
202
+ volumes_from: list[str] | None = Field(default=None, validate_default=True)
203
+ tmpfs: list[str] | None = Field(default=None, validate_default=True)
204
+ working_dir: str | None = None
205
+
206
+ # Runtime
207
+ user: str | None = None
208
+ privileged: bool | None = None
209
+ read_only: bool | None = None
210
+ stdin_open: bool | None = None
211
+ tty: bool | None = None
212
+ init: bool | None = None
213
+ restart: str | None = None
214
+ stop_grace_period: str | None = Field(default=None, validate_default=True)
215
+ stop_signal: str | None = None
216
+ runtime: str | None = None
217
+ platform: str | None = None
218
+ ipc: str | None = None
219
+ pid: str | None = None
220
+ uts: str | None = None
221
+ cgroup: str | None = None
222
+ cgroup_parent: str | None = None
223
+ isolation: str | None = None
224
+ userns_mode: str | None = None
225
+ oom_kill_disable: bool | None = None
226
+ oom_score_adj: int | None = None
227
+ pids_limit: int | None = None
228
+
229
+ # Resources
230
+ cpu_count: int | None = None
231
+ cpu_percent: int | None = None
232
+ cpu_shares: int | None = None
233
+ cpu_period: int | None = None
234
+ cpu_quota: int | None = None
235
+ cpu_rt_runtime: str | int | None = None
236
+ cpu_rt_period: str | int | None = None
237
+ cpus: float | None = None
238
+ cpuset: str | None = None
239
+ mem_limit: str | int | None = Field(default=None, validate_default=True)
240
+ mem_reservation: str | int | None = Field(default=None, validate_default=True)
241
+ mem_swappiness: int | None = None
242
+ memswap_limit: str | int | None = Field(default=None, validate_default=True)
243
+ shm_size: str | int | None = Field(default=None, validate_default=True)
244
+ storage_opt: dict[str, str] | None = None
245
+ ulimits: dict[str, int | dict[str, int]] | None = Field(
246
+ default=None, validate_default=True
247
+ )
248
+ sysctls: dict[str, str | int] | list[str] | None = Field(
249
+ default=None, validate_default=True
250
+ )
251
+ blkio_config: BlkioConfig | None = Field(default=None, validate_default=True)
252
+
253
+ # Capabilities & Security
254
+ cap_add: list[str] | None = None
255
+ cap_drop: list[str] | None = None
256
+ security_opt: list[str] | None = None
257
+ device_cgroup_rules: list[str] | None = None
258
+ devices: list[str] | None = Field(default=None, validate_default=True)
259
+ group_add: list[str] | None = None
260
+
261
+ # Health & Logging
262
+ healthcheck: Healthcheck | None = None
263
+ logging: Logging | None = None
264
+
265
+ # Dependencies
266
+ depends_on: dict[str, DependsOnConfig] | list[str] | None = Field(
267
+ default=None, validate_default=True
268
+ )
269
+ links: list[str] | None = None
270
+ external_links: list[str] | None = None
271
+
272
+ # Configs & Secrets
273
+ configs: list["ServiceConfig"] | list[str] | None = Field(
274
+ default=None, validate_default=True
275
+ )
276
+ secrets: list["ServiceSecret"] | list[str] | None = Field(
277
+ default=None, validate_default=True
278
+ )
279
+
280
+ # Metadata
281
+ container_name: str | None = None
282
+ labels: dict[str, str] | list[str] | None = Field(
283
+ default=None, validate_default=True
284
+ )
285
+ annotations: dict[str, str] | list[str] | None = Field(
286
+ default=None, validate_default=True
287
+ )
288
+ label_file: str | list[str] | None = Field(default=None, validate_default=True)
289
+
290
+ # Profiles & Scaling
291
+ profiles: list[str] | None = None
292
+ scale: int | None = None
293
+
294
+ # Build & Deploy
295
+ deploy: "DeployConfig | None" = None
296
+ develop: "DevelopConfig | None" = None
297
+
298
+ # Pull policy
299
+ pull_policy: str | None = None
300
+
301
+ # Credentials
302
+ credential_spec: CredentialSpec | None = None
303
+
304
+ # Extends
305
+ extends: ExtendsConfig | None = None
306
+
307
+ # Lifecycle hooks
308
+ post_start: list[PostStartHook] | None = None
309
+ pre_stop: list[PostStartHook] | None = None
310
+
311
+ # GPU
312
+ gpus: list[GpuConfig] | str | None = None
313
+
314
+ # Models
315
+ models: list[str] | dict[str, "ServiceModelConfig"] | None = None
316
+
317
+ # Provider
318
+ provider: ProviderConfig | None = None
319
+
320
+ # API socket
321
+ use_api_socket: bool | None = None
322
+
323
+ # Attach
324
+ attach: bool | None = None
325
+
326
+ # ------------------------------------------------------------------
327
+ # Validators
328
+ # ------------------------------------------------------------------
329
+
330
+ @field_validator("build", mode="before")
331
+ @staticmethod
332
+ def _build(v: Any) -> dict[str, Any] | str | None:
333
+ if v is None or isinstance(v, (str, dict)):
334
+ return v
335
+ return None
336
+
337
+ @field_validator("command", mode="before")
338
+ @staticmethod
339
+ def _command(v: Any) -> str | list[str] | None:
340
+ return _parse_command(v)
341
+
342
+ @field_validator("entrypoint", mode="before")
343
+ @staticmethod
344
+ def _entrypoint(v: Any) -> str | list[str] | None:
345
+ return _parse_command(v)
346
+
347
+ @field_validator("environment", mode="before")
348
+ @staticmethod
349
+ def _environment(v: Any) -> dict[str, str | None] | None:
350
+ return _parse_environment(v)
351
+
352
+ @field_validator("env_file", mode="before")
353
+ @staticmethod
354
+ def _env_file(v: Any) -> list[EnvFileEntry] | list[str] | str | None:
355
+ if v is None:
356
+ return None
357
+ if isinstance(v, str):
358
+ return v
359
+ if isinstance(v, list):
360
+ result: list[EnvFileEntry | str] = []
361
+ for item in v:
362
+ if isinstance(item, str):
363
+ result.append(item)
364
+ elif isinstance(item, dict):
365
+ result.append(EnvFileEntry.model_validate(item))
366
+ return result
367
+ return None
368
+
369
+ @field_validator("ports", mode="before")
370
+ @staticmethod
371
+ def _ports(v: Any) -> list[PortConfig] | list[str] | None:
372
+ if v is None:
373
+ return None
374
+ if isinstance(v, list):
375
+ result: list[PortConfig | str] = []
376
+ for item in v:
377
+ if isinstance(item, str):
378
+ result.append(PortConfig.model_validate(_parse_port(item)))
379
+ elif isinstance(item, dict):
380
+ result.append(PortConfig.model_validate(item))
381
+ elif isinstance(item, int):
382
+ result.append(PortConfig.model_validate({"target": item}))
383
+ return result
384
+ return None
385
+
386
+ @field_validator("extra_hosts", mode="before")
387
+ @staticmethod
388
+ def _extra_hosts(v: Any) -> dict[str, str] | list[str] | None:
389
+ return _parse_extra_hosts(v)
390
+
391
+ @field_validator("dns", mode="before")
392
+ @staticmethod
393
+ def _dns(v: Any) -> str | list[str] | None:
394
+ return _parse_dns(v)
395
+
396
+ @field_validator("dns_search", mode="before")
397
+ @staticmethod
398
+ def _dns_search(v: Any) -> str | list[str] | None:
399
+ return _parse_dns(v)
400
+
401
+ @field_validator("volumes", mode="before")
402
+ @staticmethod
403
+ def _volumes(v: Any) -> list[VolumeMount] | list[str] | None:
404
+ if v is None:
405
+ return None
406
+ if isinstance(v, list):
407
+ from .volume import VolumeMount
408
+ result: list[VolumeMount | str] = []
409
+ for item in v:
410
+ if isinstance(item, str):
411
+ result.append(VolumeMount.model_validate(_parse_service_volume(item)))
412
+ elif isinstance(item, dict):
413
+ result.append(VolumeMount.model_validate(item))
414
+ return result
415
+ return None
416
+
417
+ @field_validator("volumes_from", mode="before")
418
+ @staticmethod
419
+ def _volumes_from(v: Any) -> list[str] | None:
420
+ return _parse_volumes_from(v)
421
+
422
+ @field_validator("tmpfs", mode="before")
423
+ @staticmethod
424
+ def _tmpfs(v: Any) -> list[str] | None:
425
+ return _parse_tmpfs(v)
426
+
427
+ @field_validator("stop_grace_period", mode="before")
428
+ @staticmethod
429
+ def _stop_grace_period(v: Any) -> str | None:
430
+ return validate_duration(v) if v is not None else None
431
+
432
+ @field_validator("mem_limit", mode="before")
433
+ @staticmethod
434
+ def _mem_limit(v: Any) -> str | int | None:
435
+ return validate_byte_value(v)
436
+
437
+ @field_validator("mem_reservation", mode="before")
438
+ @staticmethod
439
+ def _mem_reservation(v: Any) -> str | int | None:
440
+ return validate_byte_value(v)
441
+
442
+ @field_validator("memswap_limit", mode="before")
443
+ @staticmethod
444
+ def _memswap_limit(v: Any) -> str | int | None:
445
+ return validate_byte_value(v)
446
+
447
+ @field_validator("shm_size", mode="before")
448
+ @staticmethod
449
+ def _shm_size(v: Any) -> str | int | None:
450
+ return validate_byte_value(v)
451
+
452
+ @field_validator("ulimits", mode="before")
453
+ @staticmethod
454
+ def _ulimits(v: Any) -> dict[str, int | dict[str, int]] | None:
455
+ return _parse_ulimits(v)
456
+
457
+ @field_validator("sysctls", mode="before")
458
+ @staticmethod
459
+ def _sysctls(v: Any) -> dict[str, str | int] | list[str] | None:
460
+ if v is None:
461
+ return None
462
+ if isinstance(v, dict):
463
+ return {str(k): v for k, v in v.items()}
464
+ if isinstance(v, list):
465
+ return [str(item) for item in v]
466
+ return None
467
+
468
+ @field_validator("blkio_config", mode="before")
469
+ @staticmethod
470
+ def _blkio_config(v: Any) -> BlkioConfig | None:
471
+ if v is None:
472
+ return None
473
+ if isinstance(v, dict):
474
+ return BlkioConfig.model_validate(v)
475
+ return None
476
+
477
+ @field_validator("devices", mode="before")
478
+ @staticmethod
479
+ def _devices(v: Any) -> list[str] | None:
480
+ return _parse_devices(v)
481
+
482
+ @field_validator("depends_on", mode="before")
483
+ @staticmethod
484
+ def _depends_on(v: Any) -> dict[str, DependsOnConfig] | list[str] | None:
485
+ if v is None:
486
+ return None
487
+ if isinstance(v, list):
488
+ return [str(item) for item in v]
489
+ if isinstance(v, dict):
490
+ result: dict[str, DependsOnConfig] = {}
491
+ for k, val in v.items():
492
+ if isinstance(val, str):
493
+ result[str(k)] = DependsOnConfig(condition=val)
494
+ elif isinstance(val, dict):
495
+ result[str(k)] = DependsOnConfig.model_validate(val)
496
+ elif isinstance(val, bool):
497
+ result[str(k)] = DependsOnConfig()
498
+ return result
499
+ return None
500
+
501
+ @field_validator("configs", mode="before")
502
+ @staticmethod
503
+ def _configs(v: Any) -> list[ServiceConfig] | list[str] | None:
504
+ if v is None:
505
+ return None
506
+ from .config import ServiceConfig
507
+ if isinstance(v, list):
508
+ result: list[ServiceConfig | str] = []
509
+ for item in v:
510
+ if isinstance(item, str):
511
+ result.append(item)
512
+ elif isinstance(item, dict):
513
+ result.append(ServiceConfig.model_validate(item))
514
+ return result
515
+ return None
516
+
517
+ @field_validator("secrets", mode="before")
518
+ @staticmethod
519
+ def _secrets(v: Any) -> list[ServiceSecret] | list[str] | None:
520
+ if v is None:
521
+ return None
522
+ from .secret import ServiceSecret
523
+ if isinstance(v, list):
524
+ result: list[ServiceSecret | str] = []
525
+ for item in v:
526
+ if isinstance(item, str):
527
+ result.append(item)
528
+ elif isinstance(item, dict):
529
+ result.append(ServiceSecret.model_validate(item))
530
+ return result
531
+ return None
532
+
533
+ @field_validator("labels", mode="before")
534
+ @staticmethod
535
+ def _labels(v: Any) -> dict[str, str] | list[str] | None:
536
+ return _parse_labels(v)
537
+
538
+ @field_validator("annotations", mode="before")
539
+ @staticmethod
540
+ def _annotations(v: Any) -> dict[str, str] | list[str] | None:
541
+ return _parse_annotations(v)
542
+
543
+ @field_validator("label_file", mode="before")
544
+ @staticmethod
545
+ def _label_file(v: Any) -> str | list[str] | None:
546
+ if v is None:
547
+ return None
548
+ if isinstance(v, str):
549
+ return v
550
+ if isinstance(v, list):
551
+ return [str(item) for item in v]
552
+ return None
553
+
554
+ @field_validator("networks", mode="before")
555
+ @staticmethod
556
+ def _networks(v: Any) -> dict[str, "ServiceNetworkConfig"] | list[str] | None:
557
+ if v is None:
558
+ return None
559
+ from .network import ServiceNetworkConfig
560
+ if isinstance(v, list):
561
+ return [str(item) for item in v]
562
+ if isinstance(v, dict):
563
+ result: dict[str, ServiceNetworkConfig] = {}
564
+ for k, val in v.items():
565
+ if val is None or val == {}:
566
+ result[str(k)] = ServiceNetworkConfig()
567
+ elif isinstance(val, dict):
568
+ result[str(k)] = ServiceNetworkConfig.model_validate(val)
569
+ return result
570
+ return None
571
+
572
+ @field_validator("gpus", mode="before")
573
+ @staticmethod
574
+ def _gpus(v: Any) -> list[GpuConfig] | str | None:
575
+ if v is None:
576
+ return None
577
+ if isinstance(v, str):
578
+ return v
579
+ if isinstance(v, list):
580
+ result: list[GpuConfig] = []
581
+ for item in v:
582
+ if isinstance(item, dict):
583
+ result.append(GpuConfig.model_validate(item))
584
+ return result
585
+ return None
586
+
587
+ @field_validator("models", mode="before")
588
+ @staticmethod
589
+ def _models(v: Any) -> list[str] | dict[str, "ServiceModelConfig"] | None:
590
+ if v is None:
591
+ return None
592
+ if isinstance(v, list):
593
+ return [str(item) for item in v]
594
+ if isinstance(v, dict):
595
+ from .model import ServiceModelConfig
596
+ return {str(k): ServiceModelConfig.model_validate(val) if isinstance(val, dict) else ServiceModelConfig() for k, val in v.items()}
597
+ return None
598
+