11"""Configuration handling for the Keboola MCP server."""
22
3+ import dataclasses
34import logging
4- import os
55from dataclasses import dataclass
6- from typing import Optional
6+ from typing import Mapping , Optional
77
88logger = logging .getLogger (__name__ )
99
1010
11- @dataclass
11+ @dataclass ( frozen = True )
1212class Config :
1313 """Server configuration."""
1414
15- storage_token : str
15+ storage_token : Optional [ str ] = None
1616 storage_api_url : str = "https://connection.keboola.com"
1717 log_level : str = "INFO"
1818 # Add Snowflake credentials
@@ -24,71 +24,42 @@ class Config:
2424 snowflake_schema : Optional [str ] = None
2525 snowflake_role : Optional [str ] = None
2626
27- def __init__ (
28- self ,
29- storage_token : str ,
30- storage_api_url : str = "https://connection.keboola.com" ,
31- snowflake_account : Optional [str ] = None ,
32- snowflake_user : Optional [str ] = None ,
33- snowflake_password : Optional [str ] = None ,
34- snowflake_warehouse : Optional [str ] = None ,
35- snowflake_database : Optional [str ] = None ,
36- snowflake_role : Optional [str ] = None ,
37- snowflake_schema : Optional [str ] = None ,
38- log_level : str = "INFO" ,
39- ):
40- self .storage_token = storage_token
41- self .storage_api_url = storage_api_url
42- self .snowflake_account = snowflake_account
43- self .snowflake_user = snowflake_user
44- self .snowflake_password = snowflake_password
45- self .snowflake_warehouse = snowflake_warehouse
46- self .snowflake_database = snowflake_database
47- self .snowflake_role = snowflake_role
48- self .snowflake_schema = snowflake_schema
49- self .log_level = log_level
27+ @classmethod
28+ def _read_options (cls , d : Mapping [str , str ]) -> Mapping [str , str ]:
29+ options : dict [str , str ] = {}
30+ for f in dataclasses .fields (cls ):
31+ if f .name in d :
32+ options [f .name ] = d .get (f .name )
33+ elif (dict_name := f"KBC_{ f .name .upper ()} " ) in d :
34+ options [f .name ] = d .get (dict_name )
35+ return options
5036
5137 @classmethod
52- def from_env (cls ) -> "Config" :
53- """Create config from environment variables."""
54- # Add debug logging using logger instead of print
55- for env_var in [
56- "KBC_SNOWFLAKE_ACCOUNT" ,
57- "KBC_SNOWFLAKE_USER" ,
58- "KBC_SNOWFLAKE_PASSWORD" ,
59- "KBC_SNOWFLAKE_WAREHOUSE" ,
60- "KBC_SNOWFLAKE_DATABASE" ,
61- "KBC_SNOWFLAKE_ROLE" ,
62- "KBC_SNOWFLAKE_SCHEMA" ,
63- ]:
64- logger .debug (f"Reading { env_var } : { 'set' if os .getenv (env_var ) else 'not set' } " )
38+ def from_dict (cls , d : Mapping [str , str ]) -> "Config" :
39+ """
40+ Creates new `Config` instance with values read from the input mapping.
41+ The keys in the input mapping can either be the names of the fields in `Config` class
42+ or their uppercase variant prefixed with 'KBC_'.
43+ """
44+ return cls (** cls ._read_options (d ))
6545
66- storage_token = os .getenv ("KBC_STORAGE_TOKEN" )
67- if not storage_token :
68- raise ValueError ("KBC_STORAGE_TOKEN environment variable is required" )
46+ def replace_by (self , d : Mapping [str , str ]) -> "Config" :
47+ """
48+ Creates new `Config` instance from the existing one by replacing the values from the input mapping.
49+ The keys in the input mapping can either be the names of the fields in `Config` class
50+ or their uppercase variant prefixed with 'KBC_'.
51+ """
52+ return dataclasses .replace (self , ** self ._read_options (d ))
6953
70- return cls (
71- storage_token = storage_token ,
72- storage_api_url = os .getenv ("KBC_STORAGE_API_URL" , "https://connection.keboola.com" ),
73- snowflake_account = os .getenv ("KBC_SNOWFLAKE_ACCOUNT" ),
74- snowflake_user = os .getenv ("KBC_SNOWFLAKE_USER" ),
75- snowflake_password = os .getenv ("KBC_SNOWFLAKE_PASSWORD" ),
76- snowflake_warehouse = os .getenv ("KBC_SNOWFLAKE_WAREHOUSE" ),
77- snowflake_database = os .getenv ("KBC_SNOWFLAKE_DATABASE" ),
78- snowflake_role = os .getenv ("KBC_SNOWFLAKE_ROLE" ),
79- snowflake_schema = os .getenv ("KBC_SNOWFLAKE_SCHEMA" ),
80- log_level = os .getenv ("KBC_LOG_LEVEL" , "INFO" ),
54+ def has_storage_config (self ) -> bool :
55+ """Check if Storage API configuration is complete."""
56+ return all (
57+ [
58+ self .storage_token ,
59+ self .storage_api_url ,
60+ ]
8161 )
8262
83- def validate (self ) -> None :
84- """Validate the configuration."""
85- if not self .storage_token :
86- raise ValueError ("Storage token not configured" )
87- if not self .storage_api_url :
88- raise ValueError ("Storage API URL is required" )
89- if self .log_level not in ["DEBUG" , "INFO" , "WARNING" , "ERROR" , "CRITICAL" ]:
90- raise ValueError (f"Invalid log level: { self .log_level } " )
91-
9263 def has_snowflake_config (self ) -> bool :
9364 """Check if Snowflake configuration is complete."""
9465 return all (
@@ -100,3 +71,16 @@ def has_snowflake_config(self) -> bool:
10071 self .snowflake_database ,
10172 ]
10273 )
74+
75+ def __repr__ (self ):
76+ params : list [str ] = []
77+ for f in dataclasses .fields (self ):
78+ value = getattr (self , f .name )
79+ if value :
80+ if "token" in f .name or "password" in f .name :
81+ params .append (f"{ f .name } ='****'" )
82+ else :
83+ params .append (f"{ f .name } ='{ value } '" )
84+ else :
85+ params .append (f"{ f .name } =None" )
86+ return f'Config({ ", " .join (params )} )'
0 commit comments