BI-analytics-admin 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.
web/config.py ADDED
@@ -0,0 +1,156 @@
1
+ """Application configuration loaded from environment variables."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from functools import lru_cache
7
+
8
+ from dotenv import load_dotenv
9
+
10
+ load_dotenv()
11
+
12
+
13
+ DEFAULT_CSP_POLICY = (
14
+ "default-src 'self' blob:; "
15
+ "script-src 'self' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com; "
16
+ "style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com; "
17
+ "img-src 'self' data: blob:; "
18
+ "connect-src 'self'; "
19
+ "font-src 'self' data:; "
20
+ "object-src 'none'; "
21
+ "base-uri 'self'; "
22
+ "frame-ancestors 'none'; "
23
+ "form-action 'self'"
24
+ )
25
+
26
+
27
+ class Settings:
28
+ """Runtime settings for the dashboard application."""
29
+
30
+ def __init__(self) -> None:
31
+ """Read all settings from environment variables."""
32
+ # MSSQL source database (read-only metadata + data)
33
+ # No defaults for host or credentials: the application resolves
34
+ # connections from per-user connectors stored in the database.
35
+ # Env vars remain only for temporary tooling/migrations.
36
+ self.DB_HOST: str = os.getenv("DB_HOST", "")
37
+ self.DB_PORT: int = int(os.getenv("DB_PORT", "1433"))
38
+ self.DB_NAME: str = os.getenv("DB_NAME", "")
39
+ self.DB_USER: str = os.getenv("DB_USER", "")
40
+ self.DB_PASSWORD: str = os.getenv("DB_PASSWORD", "")
41
+ self.DB_DRIVER: str = os.getenv("DB_DRIVER", "ODBC Driver 17 for SQL Server")
42
+
43
+ # Dashboard metadata storage backend: "sqlite" or "mssql"
44
+
45
+ self.DASHBOARD_STORAGE_BACKEND: str = os.getenv(
46
+ "DASHBOARD_STORAGE_BACKEND", "sqlite"
47
+ )
48
+ self.DASHBOARD_DB_PATH: str = os.getenv(
49
+ "DASHBOARD_DB_PATH",
50
+ os.path.join(os.path.dirname(__file__), "dashboards.sqlite3"),
51
+ )
52
+
53
+ # Optional alternative connection for the dashboards table in MSSQL.
54
+ self.DASHBOARD_DB_HOST: str | None = os.getenv("DASHBOARD_DB_HOST") or None
55
+ self.DASHBOARD_DB_PORT: int | None = (
56
+ int(os.getenv("DASHBOARD_DB_PORT"))
57
+ if os.getenv("DASHBOARD_DB_PORT")
58
+ else None
59
+ )
60
+ self.DASHBOARD_DB_NAME: str | None = os.getenv("DASHBOARD_DB_NAME") or None
61
+ self.DASHBOARD_DB_USER: str | None = os.getenv("DASHBOARD_DB_USER") or None
62
+ self.DASHBOARD_DB_PASSWORD: str | None = (
63
+ os.getenv("DASHBOARD_DB_PASSWORD") or None
64
+ )
65
+
66
+ # How to discover the list of available views.
67
+ self.VIEW_DISCOVERY_MODE: str = os.getenv("VIEW_DISCOVERY_MODE", "tabobecny_prehled")
68
+
69
+ self.APP_TITLE: str = os.getenv("APP_TITLE", "Analytics admin")
70
+ self.PAGE_SIZE: int = int(os.getenv("PAGE_SIZE", "500"))
71
+
72
+ # Public API for exported views.
73
+ self.API_KEY: str | None = os.getenv("API_KEY") or None
74
+ self.API_KEY_HEADER: str = os.getenv("API_KEY_HEADER", "X-API-Key")
75
+ self.API_ALLOWED_IPS: list[str] = [
76
+ ip.strip()
77
+ for ip in (os.getenv("API_ALLOWED_IPS") or "").split(",")
78
+ if ip.strip()
79
+ ]
80
+ self.API_TENANT: str = os.getenv("API_TENANT", "")
81
+ self.API_BASE_URL: str = os.getenv(
82
+ "API_BASE_URL",
83
+ f"https://127.0.0.1:8443/{self.API_TENANT}" if self.API_TENANT else "http://127.0.0.1:8443",
84
+ )
85
+ # Allowlist of hosts/domains that may send requests to the app.
86
+ api_base_host = (
87
+ self.API_BASE_URL.split("://", 1)[-1].split("/", 1)[0].split(":", 1)[0]
88
+ if self.API_BASE_URL
89
+ else ""
90
+ )
91
+ default_trusted_hosts = ["localhost", "127.0.0.1", "[::1]"]
92
+ if api_base_host:
93
+ default_trusted_hosts.append(api_base_host)
94
+ self.TRUSTED_HOSTS: list[str] = [
95
+ host.strip()
96
+ for host in (
97
+ os.getenv("TRUSTED_HOSTS") or ",".join(default_trusted_hosts)
98
+ ).split(",")
99
+ if host.strip()
100
+ ]
101
+ self.API_MAX_REQUESTS_PER_MINUTE: int = int(
102
+ os.getenv("API_MAX_REQUESTS_PER_MINUTE", "0")
103
+ )
104
+ self.API_LOG_PATH: str = os.getenv(
105
+ "API_LOG_PATH",
106
+ os.path.join(os.path.dirname(__file__), "..", "..", "logs", "api_access.log"),
107
+ )
108
+
109
+ # Magic-link / JWT authentication. No default secret – the app
110
+ # refuses to start without JWT_SECRET set in the environment.
111
+ self.JWT_SECRET: str = os.getenv("JWT_SECRET", "")
112
+ if not self.JWT_SECRET:
113
+ raise RuntimeError(
114
+ "JWT_SECRET environment variable is not set. "
115
+ "Generate a long random secret and add it to your .env file."
116
+ )
117
+ self.LOCALHOST_MODE: bool = os.getenv("LOCALHOST_MODE", "false").lower() in ("1", "true", "yes")
118
+ self.REGISTRATION_ENABLED: bool = os.getenv("REGISTRATION_ENABLED", "true").lower() in ("1", "true", "yes")
119
+
120
+ # Control whether magic link is shown in API response
121
+ self.SHOW_MAGIC_LINK: bool = os.getenv("SHOW_MAGIC_LINK", "true").lower() in ("1", "true", "yes")
122
+
123
+ # SMTP settings for magic-link emails
124
+ self.SMTP_HOST: str = os.getenv("SMTP_HOST", "")
125
+ self.SMTP_PORT: int = int(os.getenv("SMTP_PORT", "587"))
126
+ self.SMTP_USER: str = os.getenv("SMTP_USER", "")
127
+ self.SMTP_PASSWORD: str = os.getenv("SMTP_PASSWORD", "")
128
+ self.SMTP_FROM: str = os.getenv("SMTP_FROM", self.SMTP_USER)
129
+ self.SMTP_STARTTLS: bool = os.getenv("SMTP_STARTTLS", "true").lower() in ("1", "true", "yes")
130
+
131
+ # HTTP security hardening
132
+ self.CORS_ALLOWED_ORIGINS: list[str] = [
133
+ origin.strip()
134
+ for origin in (os.getenv("CORS_ALLOWED_ORIGINS") or "").split(",")
135
+ if origin.strip()
136
+ ]
137
+ self.CSP_POLICY: str = os.getenv("CSP_POLICY") or DEFAULT_CSP_POLICY
138
+ hsts_default = "false" if self.LOCALHOST_MODE else "true"
139
+ self.HSTS_ENABLED: bool = os.getenv("HSTS_ENABLED", hsts_default).lower() in ("1", "true", "yes")
140
+
141
+ # Uvicorn HTTP listener
142
+ self.HTTP_HOST: str = os.getenv("HTTP_HOST", "0.0.0.0")
143
+ self.HTTP_PORT: int = int(os.getenv("HTTP_PORT", "8443"))
144
+
145
+ # SSL / HTTPS: either point uvicorn at PEM files directly or extract a
146
+ # PFX (.pfx/.p12) bundle on startup into temporary PEM files.
147
+ self.SSL_CERTFILE: str = os.getenv("SSL_CERTFILE", "")
148
+ self.SSL_KEYFILE: str = os.getenv("SSL_KEYFILE", "")
149
+ self.SSL_PFX_PATH: str = os.getenv("SSL_PFX_PATH", "")
150
+ self.SSL_PFX_PASSWORD: str = os.getenv("SSL_PFX_PASSWORD", "")
151
+
152
+
153
+ @lru_cache
154
+ def get_settings() -> Settings:
155
+ """Return cached settings instance."""
156
+ return Settings()