11import contextlib
2+ import json
23import logging
34import typing
45import urllib .parse
56
7+ import chalice .constants
68import firebase_admin
79import firebase_admin .credentials
810import httpx
@@ -22,6 +24,89 @@ def log_response(resp: httpx.Response) -> None:
2224 logger .info (f"RES [{ req .method } ]{ req .url } <{ resp .status_code } > { resp .read ().decode (errors = 'ignore' )= } " )
2325
2426
27+ class ChaliceAPIGatewayCustomDomainConfig (pydantic .BaseModel ):
28+ domain_name : str
29+ certificate_arn : str
30+ tls_version : typing .Literal ["TLS_1_0" , "TLS_1_2" ] = chalice .constants .DEFAULT_TLS_VERSION # TLS_1_2
31+ url_prefix : str | None = None
32+ tags : dict [str , str ] | None = None
33+
34+
35+ class ChaliceLambdaConfig (pydantic .BaseModel ):
36+ autogen_policy : bool = True
37+ iam_policy_file : pydantic .FilePath | None = None
38+ iam_role_arn : str | None = None
39+ manage_iam_role : bool = True
40+ subnet_ids : list [str ] | None = None
41+ security_group_ids : list [str ] | None = None
42+
43+ lambda_memory_size : int | None = chalice .constants .DEFAULT_LAMBDA_MEMORY_SIZE
44+ lambda_timeout : int | None = chalice .constants .DEFAULT_LAMBDA_TIMEOUT
45+ layers : list [str ] | None = None
46+ log_retention_in_days : int | None = None
47+ reserved_concurrency : int | None = None
48+
49+ environment_variables : dict [str , str ] | None = None
50+ tags : dict [str , str ] | None = None
51+
52+ @pydantic .field_validator ("lambda_memory_size" , mode = "before" )
53+ @classmethod
54+ def validate_lambda_memory_size (cls , lambda_memory_size : int ) -> int :
55+ if lambda_memory_size % 64 != 0 :
56+ raise ValueError ("lambda_memory_size must be a multiple of 64" )
57+
58+ return lambda_memory_size
59+
60+ @pydantic .model_validator (mode = "after" )
61+ def validate_model (self ) -> typing .Self :
62+ if not (self .autogen_policy or self .iam_policy_file ):
63+ raise ValueError ("iam_policy_file must be set when autogen_policy is False" )
64+
65+ if not (self .manage_iam_role or self .iam_role_arn ):
66+ raise ValueError ("iam_role_arn must be set when manage_iam_role is False" )
67+
68+ if bool (self .subnet_ids ) != bool (self .security_group_ids ):
69+ raise ValueError ("subnet_ids and security_group_ids must be set together" )
70+
71+ return self
72+
73+
74+ class ChaliceStageConfig (ChaliceLambdaConfig , pydantic .BaseModel ):
75+ api_gateway_stage : str = chalice .constants .DEFAULT_APIGATEWAY_STAGE_NAME
76+
77+ api_gateway_custom_domain : ChaliceAPIGatewayCustomDomainConfig | None = None
78+ websocket_api_custom_domain : ChaliceAPIGatewayCustomDomainConfig | None = None
79+
80+ automatic_layer : bool = False
81+ minimum_compression_size : int | None = pydantic .Field (
82+ default = None ,
83+ ge = chalice .constants .MIN_COMPRESSION_SIZE ,
84+ le = chalice .constants .MAX_COMPRESSION_SIZE ,
85+ )
86+ xray : bool = False
87+ lambda_functions : dict [str , ChaliceLambdaConfig ] | None = None
88+
89+
90+ class ChaliceRootConfig (ChaliceStageConfig , pydantic .BaseModel ):
91+ api_gateway_endpoint_type : typing .Literal ["EDGE" , "REGIONAL" , "PRIVATE" ] = chalice .constants .DEFAULT_ENDPOINT_TYPE
92+ api_gateway_endpoint_vpce : list [str ] | str | None = None
93+ api_gateway_policy_file : pydantic .FilePath | None = None
94+
95+ stages : dict [str , ChaliceStageConfig ] | None = None
96+
97+ @pydantic .model_validator (mode = "after" )
98+ def validate_model (self ) -> typing .Self :
99+ if self .api_gateway_endpoint_type == "PRIVATE" and not self .api_gateway_endpoint_vpce :
100+ raise ValueError ("api_gateway_endpoint_vpce must be set when api_gateway_endpoint_type is PRIVATE" )
101+
102+ return self
103+
104+
105+ class ChaliceConfig (ChaliceRootConfig , pydantic .BaseModel ):
106+ version : typing .Literal ["2.0" ] = "2.0"
107+ app_name : str
108+
109+
25110class InfraConfig (pydantic_settings .BaseSettings ):
26111 ecr_repo_name : str = "notico"
27112 lambda_name : str = "notico-lambda"
@@ -66,6 +151,10 @@ def get_session(self, service: AllowedToastServices) -> httpx.Client: # type: i
66151 event_hooks = {"request" : [log_request ], "response" : [log_response ]},
67152 )
68153
154+ @pydantic .field_serializer ("secret_key" , "sender_key" , when_used = "json-unless-none" )
155+ def dump_secret (self , v : pydantic .SecretStr ) -> str :
156+ return v .get_secret_value ()
157+
69158
70159class FirebaseConfig (ServiceConfig , pydantic_settings .BaseSettings ):
71160 certificate : pydantic .SecretStr | None = None
@@ -81,6 +170,10 @@ def get_session(self) -> firebase_admin.App | None: # type: ignore[override]
81170
82171 return self ._app
83172
173+ @pydantic .field_serializer ("certificate" , when_used = "json-unless-none" )
174+ def dump_secret (self , v : pydantic .SecretStr ) -> str :
175+ return v .get_secret_value ()
176+
84177
85178class TelegramConfig (ServiceConfig , pydantic_settings .BaseSettings ):
86179 bot_token : pydantic .SecretStr | None = None
@@ -93,20 +186,70 @@ def get_session(self) -> httpx.Client:
93186 event_hooks = {"request" : [log_request ], "response" : [log_response ]},
94187 )
95188
189+ @pydantic .field_serializer ("bot_token" , when_used = "json-unless-none" )
190+ def dump_secret (self , v : pydantic .SecretStr ) -> str :
191+ return v .get_secret_value ()
192+
96193
97194class SlackConfig (ServiceConfig , pydantic_settings .BaseSettings ):
98195 channel : str | None = None
99196 token : pydantic .SecretStr | None = None
100197
198+ @pydantic .field_serializer ("token" , when_used = "json-unless-none" )
199+ def dump_secret (self , v : pydantic .SecretStr ) -> str :
200+ return v .get_secret_value ()
201+
101202
102203class Config (pydantic_settings .BaseSettings ):
204+ project_name : str = "notico"
205+
206+ chalice : ChaliceRootConfig = pydantic .Field (default_factory = ChaliceRootConfig )
103207 infra : InfraConfig = pydantic .Field (default_factory = InfraConfig )
104208 toast : ToastConfig = pydantic .Field (default_factory = ToastConfig )
105209 firebase : FirebaseConfig = pydantic .Field (default_factory = FirebaseConfig )
106210 slack : SlackConfig = pydantic .Field (default_factory = SlackConfig )
107211 telegram : TelegramConfig = pydantic .Field (default_factory = TelegramConfig )
108212
109- env_vars : dict [str , str ] = pydantic .Field (default_factory = dict )
213+ model_config = pydantic_settings .SettingsConfigDict (
214+ env_nested_delimiter = "__" ,
215+ case_sensitive = False ,
216+ arbitrary_types_allowed = True ,
217+ )
218+
219+ @property
220+ def env_dict (self ) -> dict [str , str ]:
221+ settings_dict : dict [str , typing .Any ] = self .model_dump (
222+ mode = "json" ,
223+ by_alias = True ,
224+ exclude = ["chalice" ],
225+ exclude_unset = True ,
226+ exclude_none = True ,
227+ )
228+ result_dict : dict [str , str ] = {}
229+
230+ if not (delimiters := self .model_config .get ("env_nested_delimiter" )):
231+ raise ValueError ("env_nested_delimiter must be set in model_config" )
232+
233+ def _flatten_dict (d : dict , prefix : str = "" ) -> None :
234+ for k , v in d .items ():
235+ key = f"{ prefix } { delimiters } { k } " if prefix else k
236+ if isinstance (v , dict ):
237+ _flatten_dict (v , key )
238+ elif isinstance (v , str ):
239+ result_dict [key ] = v
240+ else :
241+ result_dict [key ] = json .dumps (v )
242+
243+ _flatten_dict (settings_dict )
244+ return result_dict
245+
246+ @property
247+ def chalice_config (self ) -> ChaliceConfig :
248+ return ChaliceConfig (
249+ app_name = self .project_name ,
250+ environment_variables = self .env_dict ,
251+ ** self .chalice .model_dump (mode = "python" , exclude_unset = True , exclude_none = True ),
252+ )
110253
111254
112- config = Config (_env_nested_delimiter = "__" , _case_sensitive = False )
255+ config = Config ()
0 commit comments