44
55from datetime import timedelta
66from pathlib import Path
7- from typing import Annotated
7+ from typing import Annotated , Self
88
9- from pydantic import Field , HttpUrl , SecretStr
9+ import yaml
10+ from pydantic import BaseModel , Field , HttpUrl , model_validator
1011from pydantic_settings import BaseSettings , SettingsConfigDict
1112from safir .logging import LogLevel , Profile
1213from safir .pydantic import HumanTimedelta
1314
14- __all__ = [
15- "Config" ,
16- "config" ,
17- ]
15+ __all__ = ["Config" , "HiPSDatasetConfig" ]
16+
17+
18+ class HiPSDatasetConfig (BaseModel ):
19+ """Configuration for a single HiPS dataset."""
20+
21+ url : Annotated [
22+ HttpUrl ,
23+ Field (title = "Base URL" , description = "Base URL for this HiPS dataset" ),
24+ ]
25+
26+ paths : Annotated [
27+ list [str ],
28+ Field (
29+ title = "HiPS paths" ,
30+ description = "List of available HiPS paths" ,
31+ ),
32+ ]
1833
1934
2035class Config (BaseSettings ):
2136 """Configuration for datalinker."""
2237
23- model_config = SettingsConfigDict (
24- env_prefix = "DATALINKER_" , case_sensitive = False
25- )
38+ model_config = SettingsConfigDict (extra = "forbid" , populate_by_name = True )
2639
2740 cutout_sync_url : Annotated [
2841 HttpUrl ,
@@ -31,30 +44,65 @@ class Config(BaseSettings):
3144 description = (
3245 "URL to the sync API for the SODA service that does cutouts"
3346 ),
47+ validation_alias = "cutoutSyncUrl" ,
3448 ),
3549 ]
3650
37- hips_base_url : Annotated [HttpUrl , Field (title = "Base URL for HiPS lists" )]
51+ hips_base_url : Annotated [
52+ HttpUrl ,
53+ Field (title = "Base URL for HiPS lists" , validation_alias = "hipsBaseUrl" ),
54+ ]
55+
56+ hips_datasets : Annotated [
57+ dict [str , HiPSDatasetConfig ],
58+ Field (
59+ title = "HiPS dataset configurations" ,
60+ description = (
61+ "Mapping of dataset names to their configuration. "
62+ "Each dataset has a base URL and list of available HiPS paths."
63+ ),
64+ validation_alias = "hipsDatasets" ,
65+ ),
66+ ] = {}
67+
68+ hips_default_dataset : Annotated [
69+ str , Field (validation_alias = "hipsDefaultDataset" )
70+ ] = ""
71+ """The dataset to serve from v1 routes. Must be a key in hips_datasets"""
3872
3973 hips_path_prefix : Annotated [
4074 str ,
4175 Field (
4276 title = "URL prefix for HiPS API" ,
4377 description = "URL prefix used to inject the HiPS list file" ,
78+ validation_alias = "hipsPathPrefix" ,
4479 ),
4580 ] = "/api/hips"
4681
82+ hips_v2_path_prefix : Annotated [
83+ str ,
84+ Field (
85+ title = "URL prefix for HiPS API" ,
86+ description = "URL prefix used to inject the HiPS list file" ,
87+ validation_alias = "hipsV2PathPrefix" ,
88+ ),
89+ ] = "/api/hips/v2"
90+
4791 links_lifetime : Annotated [
4892 HumanTimedelta ,
4993 Field (
5094 title = "Lifetime of image links replies" ,
5195 description = "Should match the lifetime of signed URLs from Butler" ,
96+ validation_alias = "linksLifetime" ,
5297 ),
5398 ] = timedelta (hours = 1 )
5499
55100 log_level : Annotated [
56101 LogLevel ,
57- Field (title = "Log level of the application's logger" ),
102+ Field (
103+ title = "Log level of the application's logger" ,
104+ validation_alias = "logLevel" ,
105+ ),
58106 ] = LogLevel .INFO
59107
60108 name : Annotated [str , Field (title = "Application name" )] = "datalinker"
@@ -67,6 +115,7 @@ class Config(BaseSettings):
67115 "This URL prefix is used for the IVOA DataLink API and for"
68116 " any other helper APIs exposed via DataLink descriptors"
69117 ),
118+ validation_alias = "pathPrefix" ,
70119 ),
71120 ] = "/api/datalink"
72121
@@ -75,28 +124,100 @@ class Config(BaseSettings):
75124 Field (title = "Application logging profile" ),
76125 ] = Profile .production
77126
127+ tap_metadata_url : Annotated [
128+ Path | None ,
129+ Field (
130+ title = "URL to TAP schema metadata" ,
131+ description = (
132+ "URL containing TAP schema metadata used to construct queries"
133+ ),
134+ validation_alias = "tapMetadataUrl" ,
135+ ),
136+ ] = None
137+
78138 tap_metadata_dir : Annotated [
79139 Path | None ,
80140 Field (
81141 title = "Path to TAP YAML metadata" ,
82142 description = (
83143 "Directory containing YAML metadata files about TAP schema"
84144 ),
145+ validation_alias = "tapMetadataDir" ,
85146 ),
86147 ] = None
87148
88- slack_webhook : Annotated [
89- SecretStr | None , Field (title = "Slack webhook for exception reporting" )
90- ] = None
149+ slack_alerts : bool = Field (
150+ False ,
151+ title = "Enable Slack alerts" ,
152+ description = (
153+ "Whether to enable Slack alerts. If true, ``slack_webhook`` must"
154+ " also be set."
155+ ),
156+ validation_alias = "slackAlerts" ,
157+ )
91158
92- token : Annotated [
93- str ,
94- Field (
95- title = "Token for API authentication" ,
96- description = "Token to use to authenticate to the HiPS service" ,
159+ slack_webhook : str | None = Field (
160+ None ,
161+ title = "Slack webhook for alerts" ,
162+ description = (
163+ "If set, failures creating user labs or file servers and any"
164+ " uncaught exceptions in the Nublado controller will be"
165+ " reported to Slack via this webhook"
97166 ),
98- ]
167+ validation_alias = "DATALINKER_SLACK_WEBHOOK" ,
168+ )
99169
170+ token : str = Field (
171+ title = "Token for API authentication" ,
172+ description = "Token to use to authenticate to the HiPS service" ,
173+ validation_alias = "DATALINKER_TOKEN" ,
174+ )
100175
101- config = Config ()
102- """Configuration for datalinker."""
176+ def has_hips_datasets (self ) -> bool :
177+ """Check if any HiPS datasets are configured."""
178+ return bool (self .hips_datasets )
179+
180+ def get_default_hips_dataset (self ) -> HiPSDatasetConfig :
181+ """Return the HiPS dataset config for the default dataset.
182+
183+ Returns
184+ -------
185+ HiPSDatasetConfig | None
186+ The default dataset configuration, or None if not configured.
187+ """
188+ return self .hips_datasets [self .hips_default_dataset ]
189+
190+ @model_validator (mode = "after" )
191+ def validate_default_hips_dataset (self ) -> Self :
192+ """Validate that the default HiPS dataset exists if specified."""
193+ if self .hips_default_dataset :
194+ if not self .hips_datasets :
195+ msg = (
196+ f"HiPS dataset key { self .hips_default_dataset } specified "
197+ "but no datasets are configured in hips_datasets"
198+ )
199+ raise ValueError (msg )
200+ if self .hips_default_dataset not in self .hips_datasets :
201+ msg = (
202+ f"HiPS dataset key { self .hips_default_dataset } not found. "
203+ f"Available datasets: { list (self .hips_datasets .keys ())} "
204+ )
205+ raise ValueError (msg )
206+ return self
207+
208+ @classmethod
209+ def from_file (cls , path : Path ) -> Self :
210+ """Construct a Configuration object from a configuration file.
211+
212+ Parameters
213+ ----------
214+ path
215+ Path to the configuration file in YAML.
216+
217+ Returns
218+ -------
219+ Config
220+ The corresponding `Configuration` object.
221+ """
222+ with path .open ("r" ) as f :
223+ return cls .model_validate (yaml .safe_load (f ))
0 commit comments