squirrels 0.1.0__py3-none-any.whl → 0.6.0.post0__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.
- dateutils/__init__.py +6 -0
- dateutils/_enums.py +25 -0
- squirrels/dateutils.py → dateutils/_implementation.py +409 -380
- dateutils/types.py +6 -0
- squirrels/__init__.py +21 -18
- squirrels/_api_routes/__init__.py +5 -0
- squirrels/_api_routes/auth.py +337 -0
- squirrels/_api_routes/base.py +196 -0
- squirrels/_api_routes/dashboards.py +156 -0
- squirrels/_api_routes/data_management.py +148 -0
- squirrels/_api_routes/datasets.py +220 -0
- squirrels/_api_routes/project.py +289 -0
- squirrels/_api_server.py +552 -134
- squirrels/_arguments/__init__.py +0 -0
- squirrels/_arguments/init_time_args.py +83 -0
- squirrels/_arguments/run_time_args.py +111 -0
- squirrels/_auth.py +777 -0
- squirrels/_command_line.py +239 -107
- squirrels/_compile_prompts.py +147 -0
- squirrels/_connection_set.py +94 -0
- squirrels/_constants.py +141 -64
- squirrels/_dashboards.py +179 -0
- squirrels/_data_sources.py +570 -0
- squirrels/_dataset_types.py +91 -0
- squirrels/_env_vars.py +209 -0
- squirrels/_exceptions.py +29 -0
- squirrels/_http_error_responses.py +52 -0
- squirrels/_initializer.py +319 -110
- squirrels/_logging.py +121 -0
- squirrels/_manifest.py +357 -187
- squirrels/_mcp_server.py +578 -0
- squirrels/_model_builder.py +69 -0
- squirrels/_model_configs.py +74 -0
- squirrels/_model_queries.py +52 -0
- squirrels/_models.py +1201 -0
- squirrels/_package_data/base_project/.env +7 -0
- squirrels/_package_data/base_project/.env.example +44 -0
- squirrels/_package_data/base_project/connections.yml +16 -0
- squirrels/_package_data/base_project/dashboards/dashboard_example.py +40 -0
- squirrels/_package_data/base_project/dashboards/dashboard_example.yml +22 -0
- squirrels/_package_data/base_project/docker/.dockerignore +16 -0
- squirrels/_package_data/base_project/docker/Dockerfile +16 -0
- squirrels/_package_data/base_project/docker/compose.yml +7 -0
- squirrels/_package_data/base_project/duckdb_init.sql +10 -0
- squirrels/_package_data/base_project/gitignore +13 -0
- squirrels/_package_data/base_project/macros/macros_example.sql +17 -0
- squirrels/_package_data/base_project/models/builds/build_example.py +26 -0
- squirrels/_package_data/base_project/models/builds/build_example.sql +16 -0
- squirrels/_package_data/base_project/models/builds/build_example.yml +57 -0
- squirrels/_package_data/base_project/models/dbviews/dbview_example.sql +17 -0
- squirrels/_package_data/base_project/models/dbviews/dbview_example.yml +32 -0
- squirrels/_package_data/base_project/models/federates/federate_example.py +51 -0
- squirrels/_package_data/base_project/models/federates/federate_example.sql +21 -0
- squirrels/_package_data/base_project/models/federates/federate_example.yml +65 -0
- squirrels/_package_data/base_project/models/sources.yml +38 -0
- squirrels/_package_data/base_project/parameters.yml +142 -0
- squirrels/_package_data/base_project/pyconfigs/connections.py +19 -0
- squirrels/_package_data/base_project/pyconfigs/context.py +96 -0
- squirrels/_package_data/base_project/pyconfigs/parameters.py +141 -0
- squirrels/_package_data/base_project/pyconfigs/user.py +56 -0
- squirrels/_package_data/base_project/resources/expenses.db +0 -0
- squirrels/_package_data/base_project/resources/public/.gitkeep +0 -0
- squirrels/_package_data/base_project/resources/weather.db +0 -0
- squirrels/_package_data/base_project/seeds/seed_categories.csv +6 -0
- squirrels/_package_data/base_project/seeds/seed_categories.yml +15 -0
- squirrels/_package_data/base_project/seeds/seed_subcategories.csv +15 -0
- squirrels/_package_data/base_project/seeds/seed_subcategories.yml +21 -0
- squirrels/_package_data/base_project/squirrels.yml.j2 +61 -0
- squirrels/_package_data/base_project/tmp/.gitignore +2 -0
- squirrels/_package_data/templates/login_successful.html +53 -0
- squirrels/_package_data/templates/squirrels_studio.html +22 -0
- squirrels/_package_loader.py +29 -0
- squirrels/_parameter_configs.py +592 -0
- squirrels/_parameter_options.py +348 -0
- squirrels/_parameter_sets.py +207 -0
- squirrels/_parameters.py +1703 -0
- squirrels/_project.py +796 -0
- squirrels/_py_module.py +122 -0
- squirrels/_request_context.py +33 -0
- squirrels/_schemas/__init__.py +0 -0
- squirrels/_schemas/auth_models.py +83 -0
- squirrels/_schemas/query_param_models.py +70 -0
- squirrels/_schemas/request_models.py +26 -0
- squirrels/_schemas/response_models.py +286 -0
- squirrels/_seeds.py +97 -0
- squirrels/_sources.py +112 -0
- squirrels/_utils.py +540 -149
- squirrels/_version.py +1 -3
- squirrels/arguments.py +7 -0
- squirrels/auth.py +4 -0
- squirrels/connections.py +3 -0
- squirrels/dashboards.py +3 -0
- squirrels/data_sources.py +14 -282
- squirrels/parameter_options.py +13 -189
- squirrels/parameters.py +14 -801
- squirrels/types.py +18 -0
- squirrels-0.6.0.post0.dist-info/METADATA +148 -0
- squirrels-0.6.0.post0.dist-info/RECORD +101 -0
- {squirrels-0.1.0.dist-info → squirrels-0.6.0.post0.dist-info}/WHEEL +1 -2
- {squirrels-0.1.0.dist-info → squirrels-0.6.0.post0.dist-info}/entry_points.txt +1 -0
- squirrels-0.6.0.post0.dist-info/licenses/LICENSE +201 -0
- squirrels/_credentials_manager.py +0 -87
- squirrels/_module_loader.py +0 -37
- squirrels/_parameter_set.py +0 -151
- squirrels/_renderer.py +0 -286
- squirrels/_timed_imports.py +0 -37
- squirrels/connection_set.py +0 -126
- squirrels/package_data/base_project/.gitignore +0 -4
- squirrels/package_data/base_project/connections.py +0 -21
- squirrels/package_data/base_project/database/sample_database.db +0 -0
- squirrels/package_data/base_project/database/seattle_weather.db +0 -0
- squirrels/package_data/base_project/datasets/sample_dataset/context.py +0 -8
- squirrels/package_data/base_project/datasets/sample_dataset/database_view1.py +0 -23
- squirrels/package_data/base_project/datasets/sample_dataset/database_view1.sql.j2 +0 -7
- squirrels/package_data/base_project/datasets/sample_dataset/final_view.py +0 -10
- squirrels/package_data/base_project/datasets/sample_dataset/final_view.sql.j2 +0 -2
- squirrels/package_data/base_project/datasets/sample_dataset/parameters.py +0 -30
- squirrels/package_data/base_project/datasets/sample_dataset/selections.cfg +0 -6
- squirrels/package_data/base_project/squirrels.yaml +0 -26
- squirrels/package_data/static/favicon.ico +0 -0
- squirrels/package_data/static/script.js +0 -234
- squirrels/package_data/static/style.css +0 -110
- squirrels/package_data/templates/index.html +0 -32
- squirrels-0.1.0.dist-info/LICENSE +0 -22
- squirrels-0.1.0.dist-info/METADATA +0 -67
- squirrels-0.1.0.dist-info/RECORD +0 -40
- squirrels-0.1.0.dist-info/top_level.txt +0 -1
squirrels/_env_vars.py
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
from typing import Any, Literal, Optional
|
|
2
|
+
from typing_extensions import Self
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from pydantic import BaseModel, Field, field_validator, model_validator, ConfigDict
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
from . import _constants as c
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SquirrelsEnvVars(BaseModel):
|
|
11
|
+
"""
|
|
12
|
+
Pydantic model for managing and validating Squirrels environment variables.
|
|
13
|
+
These variables are typically loaded from .env files or the system environment.
|
|
14
|
+
"""
|
|
15
|
+
model_config = ConfigDict(serialize_by_alias=True)
|
|
16
|
+
project_path: str
|
|
17
|
+
|
|
18
|
+
# Security
|
|
19
|
+
secret_key: Optional[str] = Field(
|
|
20
|
+
None, alias=c.SQRL_SECRET_KEY,
|
|
21
|
+
description="Secret key for JWT encoding/decoding and other security operations"
|
|
22
|
+
)
|
|
23
|
+
secret_admin_password: Optional[str] = Field(
|
|
24
|
+
None, alias=c.SQRL_SECRET_ADMIN_PASSWORD,
|
|
25
|
+
description="Password for the admin user"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# Auth
|
|
29
|
+
auth_db_file_path: str = Field(
|
|
30
|
+
"{project_path}/target/auth.sqlite", alias=c.SQRL_AUTH_DB_FILE_PATH,
|
|
31
|
+
description="Path to the SQLite authentication database"
|
|
32
|
+
)
|
|
33
|
+
auth_token_expire_minutes: float = Field(
|
|
34
|
+
30, ge=0, alias=c.SQRL_AUTH_TOKEN_EXPIRE_MINUTES,
|
|
35
|
+
description="Expiration time for access tokens in minutes"
|
|
36
|
+
)
|
|
37
|
+
auth_credential_origins: list[str] = Field(
|
|
38
|
+
["https://squirrels-analytics.github.io"], alias=c.SQRL_AUTH_CREDENTIAL_ORIGINS,
|
|
39
|
+
description="Allowed origins for credentials (cookies)"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# Permissions
|
|
43
|
+
elevated_access_level: Literal["admin", "member", "guest"] = Field(
|
|
44
|
+
"admin", alias=c.SQRL_PERMISSIONS_ELEVATED_ACCESS_LEVEL,
|
|
45
|
+
description="Minimum access level to access the studio"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Parameters Cache
|
|
49
|
+
parameters_cache_size: int = Field(
|
|
50
|
+
1024, ge=0, alias=c.SQRL_PARAMETERS_CACHE_SIZE,
|
|
51
|
+
description="Cache size for parameter configs"
|
|
52
|
+
)
|
|
53
|
+
parameters_cache_ttl_minutes: float = Field(
|
|
54
|
+
60, gt=0, alias=c.SQRL_PARAMETERS_CACHE_TTL_MINUTES,
|
|
55
|
+
description="Cache TTL for parameter configs in minutes"
|
|
56
|
+
)
|
|
57
|
+
parameters_datasource_refresh_minutes: float = Field(
|
|
58
|
+
60, alias=c.SQRL_PARAMETERS_DATASOURCE_REFRESH_MINUTES,
|
|
59
|
+
description="Interval in minutes for refreshing data sources. A non-positive value disables auto-refresh"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# Datasets Cache
|
|
63
|
+
datasets_cache_size: int = Field(
|
|
64
|
+
128, ge=0, alias=c.SQRL_DATASETS_CACHE_SIZE,
|
|
65
|
+
description="Cache size for dataset results"
|
|
66
|
+
)
|
|
67
|
+
datasets_cache_ttl_minutes: float = Field(
|
|
68
|
+
60, gt=0, alias=c.SQRL_DATASETS_CACHE_TTL_MINUTES,
|
|
69
|
+
description="Cache TTL for dataset results in minutes"
|
|
70
|
+
)
|
|
71
|
+
datasets_max_rows_for_ai: int = Field(
|
|
72
|
+
100, ge=0, alias=c.SQRL_DATASETS_MAX_ROWS_FOR_AI,
|
|
73
|
+
description="Max rows for AI queries"
|
|
74
|
+
)
|
|
75
|
+
datasets_max_rows_output: int = Field(
|
|
76
|
+
100000, ge=0, alias=c.SQRL_DATASETS_MAX_ROWS_OUTPUT,
|
|
77
|
+
description="Max rows for dataset output"
|
|
78
|
+
)
|
|
79
|
+
datasets_sql_timeout_seconds: float = Field(
|
|
80
|
+
2.0, gt=0, alias=c.SQRL_DATASETS_SQL_TIMEOUT_SECONDS,
|
|
81
|
+
description="Timeout for SQL operations in seconds"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Dashboards Cache
|
|
85
|
+
dashboards_cache_size: int = Field(
|
|
86
|
+
128, ge=0, alias=c.SQRL_DASHBOARDS_CACHE_SIZE,
|
|
87
|
+
description="Cache size for dashboards"
|
|
88
|
+
)
|
|
89
|
+
dashboards_cache_ttl_minutes: float = Field(
|
|
90
|
+
60, gt=0, alias=c.SQRL_DASHBOARDS_CACHE_TTL_MINUTES,
|
|
91
|
+
description="Cache TTL for dashboards in minutes"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Seeds
|
|
95
|
+
seeds_infer_schema: bool = Field(
|
|
96
|
+
True, alias=c.SQRL_SEEDS_INFER_SCHEMA,
|
|
97
|
+
description="Whether to infer schema for seeds"
|
|
98
|
+
)
|
|
99
|
+
seeds_na_values: list[str] = Field(
|
|
100
|
+
["NA"], alias=c.SQRL_SEEDS_NA_VALUES,
|
|
101
|
+
description="List of N/A values for seeds"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Connections
|
|
105
|
+
connections_default_name_used: str = Field(
|
|
106
|
+
"default", alias=c.SQRL_CONNECTIONS_DEFAULT_NAME_USED,
|
|
107
|
+
description="Default connection name to use"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# VDL
|
|
111
|
+
vdl_catalog_db_path: str = Field(
|
|
112
|
+
"ducklake:{project_path}/target/vdl_catalog.duckdb", alias=c.SQRL_VDL_CATALOG_DB_PATH,
|
|
113
|
+
description="Path to the DuckDB catalog database"
|
|
114
|
+
)
|
|
115
|
+
vdl_data_path: str = Field(
|
|
116
|
+
"{project_path}/target/vdl_data/", alias=c.SQRL_VDL_DATA_PATH,
|
|
117
|
+
description="Path to the VDL data directory"
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# Studio
|
|
121
|
+
studio_base_url: str = Field(
|
|
122
|
+
"https://squirrels-analytics.github.io/squirrels-studio-v2", alias=c.SQRL_STUDIO_BASE_URL,
|
|
123
|
+
description="Base URL for Squirrels Studio"
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# Logging
|
|
127
|
+
logging_level: str = Field(
|
|
128
|
+
"INFO", alias=c.SQRL_LOGGING_LEVEL,
|
|
129
|
+
description="Logging level"
|
|
130
|
+
)
|
|
131
|
+
logging_format: str = Field(
|
|
132
|
+
"text", alias=c.SQRL_LOGGING_FORMAT,
|
|
133
|
+
description="Logging format"
|
|
134
|
+
)
|
|
135
|
+
logging_to_file: bool | str = Field(
|
|
136
|
+
False, alias=c.SQRL_LOGGING_TO_FILE,
|
|
137
|
+
description="Whether to log to file. Can be set to true to use the default 'logs/' folder, or a folder path to write to a custom folder."
|
|
138
|
+
)
|
|
139
|
+
logging_file_size_mb: float = Field(
|
|
140
|
+
50, gt=0, alias=c.SQRL_LOGGING_FILE_SIZE_MB,
|
|
141
|
+
description="Max log file size in MB"
|
|
142
|
+
)
|
|
143
|
+
logging_file_backup_count: int = Field(
|
|
144
|
+
1, ge=0, alias=c.SQRL_LOGGING_FILE_BACKUP_COUNT,
|
|
145
|
+
description="Number of backup log files to keep"
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
@field_validator("project_path")
|
|
149
|
+
@classmethod
|
|
150
|
+
def validate_project_path_exists(cls, v: str) -> str:
|
|
151
|
+
"""Validate that the project_path is a folder that contains a squirrels.yml file."""
|
|
152
|
+
path = Path(v)
|
|
153
|
+
if not path.exists():
|
|
154
|
+
raise ValueError(f"Project path does not exist: {v}")
|
|
155
|
+
if not path.is_dir():
|
|
156
|
+
raise ValueError(f"Project path must be a directory, not a file: {v}")
|
|
157
|
+
# squirrels_yml = path / c.MANIFEST_FILE
|
|
158
|
+
# if not squirrels_yml.exists():
|
|
159
|
+
# raise ValueError(f"Project path must contain a {c.MANIFEST_FILE} file: {v}")
|
|
160
|
+
return v
|
|
161
|
+
|
|
162
|
+
@field_validator("auth_credential_origins", mode="before")
|
|
163
|
+
@classmethod
|
|
164
|
+
def parse_origins(cls, v: Any) -> list[str]:
|
|
165
|
+
if isinstance(v, str):
|
|
166
|
+
res = [x.strip() for x in v.split(",") if x.strip()]
|
|
167
|
+
return res or ["https://squirrels-analytics.github.io"]
|
|
168
|
+
return v
|
|
169
|
+
|
|
170
|
+
@field_validator("seeds_na_values", mode="before")
|
|
171
|
+
@classmethod
|
|
172
|
+
def parse_json_list(cls, v: Any) -> list[str]:
|
|
173
|
+
if isinstance(v, str):
|
|
174
|
+
try:
|
|
175
|
+
parsed = json.loads(v)
|
|
176
|
+
if not isinstance(parsed, list):
|
|
177
|
+
raise ValueError(f"The {c.SQRL_SEEDS_NA_VALUES} environment variable must be a JSON list")
|
|
178
|
+
return parsed
|
|
179
|
+
except json.JSONDecodeError:
|
|
180
|
+
return []
|
|
181
|
+
return v
|
|
182
|
+
|
|
183
|
+
@field_validator("logging_to_file", mode="before")
|
|
184
|
+
@classmethod
|
|
185
|
+
def parse_logging_to_file(cls, v: Any) -> bool | str:
|
|
186
|
+
if isinstance(v, str):
|
|
187
|
+
v_lower = v.lower()
|
|
188
|
+
if v_lower in ("true", "t", "1", "yes", "y", "on"):
|
|
189
|
+
return True
|
|
190
|
+
if v_lower in ("false", "f", "0", "no", "n", "off"):
|
|
191
|
+
return False
|
|
192
|
+
return v
|
|
193
|
+
return bool(v)
|
|
194
|
+
|
|
195
|
+
@field_validator("seeds_infer_schema", mode="before")
|
|
196
|
+
@classmethod
|
|
197
|
+
def parse_bool(cls, v: Any) -> bool:
|
|
198
|
+
if isinstance(v, str):
|
|
199
|
+
return v.lower() in ("true", "t", "1", "yes", "y", "on")
|
|
200
|
+
return bool(v)
|
|
201
|
+
|
|
202
|
+
@model_validator(mode="after")
|
|
203
|
+
def format_paths_with_filepath(self) -> Self:
|
|
204
|
+
"""Format paths containing {filepath} placeholder with the actual filepath value."""
|
|
205
|
+
self.auth_db_file_path = self.auth_db_file_path.format(project_path=self.project_path)
|
|
206
|
+
self.vdl_catalog_db_path = self.vdl_catalog_db_path.format(project_path=self.project_path)
|
|
207
|
+
self.vdl_data_path = self.vdl_data_path.format(project_path=self.project_path)
|
|
208
|
+
return self
|
|
209
|
+
|
squirrels/_exceptions.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
class InvalidInputError(Exception):
|
|
2
|
+
"""
|
|
3
|
+
Use this exception when the error is due to providing invalid inputs to the REST API
|
|
4
|
+
|
|
5
|
+
Attributes:
|
|
6
|
+
status_code: The HTTP status code to return
|
|
7
|
+
error: A short error message that should never change in the future
|
|
8
|
+
error_description: A detailed error message (that is allowed to change in the future)
|
|
9
|
+
"""
|
|
10
|
+
def __init__(self, status_code: int, error: str, error_description: str, *args) -> None:
|
|
11
|
+
self.status_code = status_code
|
|
12
|
+
self.error = error
|
|
13
|
+
self.error_description = error_description
|
|
14
|
+
super().__init__(error_description, *args)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ConfigurationError(Exception):
|
|
18
|
+
"""
|
|
19
|
+
Use this exception when the server error is due to errors in the squirrels project instead of the squirrels framework/library
|
|
20
|
+
"""
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class FileExecutionError(Exception):
|
|
25
|
+
def __init__(self, message: str, error: Exception, *args) -> None:
|
|
26
|
+
t = " "
|
|
27
|
+
new_message = f"\n" + message + f"\n{t}Produced error message:\n{t}{t}{error} (see above for more details on handled exception)"
|
|
28
|
+
super().__init__(new_message, *args)
|
|
29
|
+
self.error = error
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from starlette.requests import Request
|
|
4
|
+
from starlette.responses import JSONResponse
|
|
5
|
+
|
|
6
|
+
from ._exceptions import InvalidInputError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _strip_path_suffix_from_base_url(request: Request, *, strip_path_suffix: str | None) -> str:
|
|
10
|
+
"""
|
|
11
|
+
Return a base URL with a known mount suffix removed.
|
|
12
|
+
|
|
13
|
+
In Squirrels, the main app mounts sub-apps like `/api/0` and `/mcp`. When we're
|
|
14
|
+
handling a request inside those sub-apps, `request.base_url` includes the mount
|
|
15
|
+
path. For `WWW-Authenticate` we want to point to a top-level endpoint on the main
|
|
16
|
+
app (same mount as the main app itself), so we strip only the *sub-app mount*
|
|
17
|
+
suffix (e.g. `/api/0` or `/mcp`), not any outer mount path.
|
|
18
|
+
"""
|
|
19
|
+
base_url = str(request.base_url).rstrip("/")
|
|
20
|
+
if strip_path_suffix:
|
|
21
|
+
suffix = strip_path_suffix.rstrip("/")
|
|
22
|
+
if suffix and base_url.endswith(suffix):
|
|
23
|
+
base_url = base_url[: -len(suffix)]
|
|
24
|
+
return base_url.rstrip("/")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def invalid_input_error_to_json_response(
|
|
28
|
+
request: Request,
|
|
29
|
+
exc: InvalidInputError,
|
|
30
|
+
*,
|
|
31
|
+
oauth_resource_metadata_path: str = "/.well-known/oauth-protected-resource",
|
|
32
|
+
strip_path_suffix: str | None = None,
|
|
33
|
+
is_mcp: bool = False,
|
|
34
|
+
) -> JSONResponse:
|
|
35
|
+
"""
|
|
36
|
+
Convert an InvalidInputError into the standard Squirrels JSON error response.
|
|
37
|
+
|
|
38
|
+
For 401s, also sets `WWW-Authenticate` with a top-level `resource_metadata` URL.
|
|
39
|
+
"""
|
|
40
|
+
response = JSONResponse(
|
|
41
|
+
status_code=exc.status_code,
|
|
42
|
+
content={"error": exc.error, "error_description": exc.error_description},
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
if exc.status_code == 401:
|
|
46
|
+
top_level_base_url = _strip_path_suffix_from_base_url(request, strip_path_suffix=strip_path_suffix)
|
|
47
|
+
resource_metadata_url = f"{top_level_base_url}{oauth_resource_metadata_path}"
|
|
48
|
+
realm = "mcp" if is_mcp else "api"
|
|
49
|
+
response.headers["WWW-Authenticate"] = f'Bearer realm="{realm}", resource_metadata="{resource_metadata_url}"'
|
|
50
|
+
|
|
51
|
+
return response
|
|
52
|
+
|