sql-dialect-master 1.1.6__tar.gz → 1.1.7__tar.gz
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.
- {sql_dialect_master-1.1.6/sql_dialect_master.egg-info → sql_dialect_master-1.1.7}/PKG-INFO +15 -2
- {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/README.md +14 -1
- sql_dialect_master-1.1.7/backend/api/client_identity.py +57 -0
- {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/api/main.py +8 -1
- {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/api/middleware.py +43 -6
- {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/api/rate_limit_store.py +2 -2
- {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/config.py +36 -0
- {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/metadata.py +2 -2
- {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/pyproject.toml +1 -1
- {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7/sql_dialect_master.egg-info}/PKG-INFO +15 -2
- {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/sql_dialect_master.egg-info/SOURCES.txt +1 -0
- {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/LICENSE +0 -0
- {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/__init__.py +0 -0
- {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/api/__init__.py +0 -0
- {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/api/auth.py +0 -0
- {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/api/readiness.py +0 -0
- {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/api/workers.py +0 -0
- {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/__init__.py +0 -0
- {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/cache.py +0 -0
- {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/column_hint_validation.py +0 -0
- {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/constants.py +0 -0
- {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/exceptions.py +0 -0
- {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/function_call_scanner.py +0 -0
- {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/function_semantics.py +0 -0
- {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/functions_db.json +0 -0
- {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/functions_lookup.py +0 -0
- {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/nl2sql.py +0 -0
- {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/nl2sql_components/__init__.py +0 -0
- {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/nl2sql_components/aggregations.py +0 -0
- {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/nl2sql_components/boolean_conditions.py +0 -0
- {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/nl2sql_components/core_extract.py +0 -0
- {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/nl2sql_components/evidence.py +0 -0
- {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/nl2sql_components/mappings.py +0 -0
- {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/nl2sql_components/relations.py +0 -0
- {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/nl2sql_components/templates.py +0 -0
- {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/nl2sql_components/tokenizer.py +0 -0
- {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/nl2sql_legacy.py +0 -0
- {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/nl2sql_pipeline/__init__.py +0 -0
- {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/nl2sql_pipeline/aggregations.py +0 -0
- {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/nl2sql_pipeline/clause_extract.py +0 -0
- {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/nl2sql_pipeline/dialect.py +0 -0
- {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/nl2sql_pipeline/intent.py +0 -0
- {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/nl2sql_pipeline/ir.py +0 -0
- {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/nl2sql_pipeline/models.py +0 -0
- {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/nl2sql_pipeline/orchestrator.py +0 -0
- {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/nl2sql_pipeline/predicates.py +0 -0
- {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/nl2sql_pipeline/renderer.py +0 -0
- {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/nl2sql_pipeline/resolution.py +0 -0
- {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/nl2sql_pipeline/suggestions.py +0 -0
- {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/nl2sql_pipeline/templates.py +0 -0
- {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/nl2sql_pipeline/validation.py +0 -0
- {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/p1_sql_scanner.py +0 -0
- {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/p2_data_validation.py +0 -0
- {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/parser.py +0 -0
- {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/post_processor.py +0 -0
- {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/rules.py +0 -0
- {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/semantic_diff.py +0 -0
- {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/sql_statements.py +0 -0
- {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/transpiler.py +0 -0
- {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/type_mapping.json +0 -0
- {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/type_mapping.py +0 -0
- {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/utils/__init__.py +0 -0
- {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/utils/validators.py +0 -0
- {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/setup.cfg +0 -0
- {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/sql_dialect_master.egg-info/dependency_links.txt +0 -0
- {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/sql_dialect_master.egg-info/requires.txt +0 -0
- {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/sql_dialect_master.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sql-dialect-master
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.7
|
|
4
4
|
Summary: SQL Dialect Master - Multi-database SQL conversion engine
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -35,7 +35,7 @@ Dynamic: license-file
|
|
|
35
35
|
|
|
36
36
|
Enterprise-grade multi-database SQL conversion engine supporting 12 database dialects.
|
|
37
37
|
|
|
38
|
-
Current release: 1.1.
|
|
38
|
+
Current release: 1.1.7
|
|
39
39
|
|
|
40
40
|
[](https://www.python.org/downloads/)
|
|
41
41
|
[](https://opensource.org/licenses/MIT)
|
|
@@ -359,3 +359,16 @@ Rate limiting is on by default. The two backends are:
|
|
|
359
359
|
- `SDM_RATE_LIMIT_BACKEND=redis` — shared, atomic limits across all workers. Set `SDM_REDIS_URL` when using this backend. The store connects and pings Redis at startup and fails fast if it is unreachable; the `redis` package is required (install with `pip install redis`).
|
|
360
360
|
|
|
361
361
|
`SDM_RATE_LIMIT_ENABLED=false` disables enforcement entirely. Use the in-memory backend for local development and the Redis backend for any deployment with more than one worker or instance that must enforce a single global limit.
|
|
362
|
+
|
|
363
|
+
By default, the API does not trust `X-Forwarded-For`; rate limiting and logs
|
|
364
|
+
use the direct peer address. Set `SDM_TRUSTED_PROXIES` to the proxy IPs/CIDRs
|
|
365
|
+
only when the direct peer is a trusted proxy, after which the rightmost
|
|
366
|
+
non-trusted address in the XFF chain is used. Deployments behind a proxy must
|
|
367
|
+
also configure `SDM_HEALTH_PROBE_TOKEN` for readiness probes; readiness never
|
|
368
|
+
uses XFF for authorization.
|
|
369
|
+
|
|
370
|
+
SQL-bearing POST responses and health probes use `Cache-Control: no-store`;
|
|
371
|
+
public catalog responses use short `private` caching. Full deployment details
|
|
372
|
+
will be documented in `docs/deployment.md`.
|
|
373
|
+
|
|
374
|
+
See [`docs/deployment.md`](docs/deployment.md) for production deployment guidance.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Enterprise-grade multi-database SQL conversion engine supporting 12 database dialects.
|
|
4
4
|
|
|
5
|
-
Current release: 1.1.
|
|
5
|
+
Current release: 1.1.7
|
|
6
6
|
|
|
7
7
|
[](https://www.python.org/downloads/)
|
|
8
8
|
[](https://opensource.org/licenses/MIT)
|
|
@@ -326,3 +326,16 @@ Rate limiting is on by default. The two backends are:
|
|
|
326
326
|
- `SDM_RATE_LIMIT_BACKEND=redis` — shared, atomic limits across all workers. Set `SDM_REDIS_URL` when using this backend. The store connects and pings Redis at startup and fails fast if it is unreachable; the `redis` package is required (install with `pip install redis`).
|
|
327
327
|
|
|
328
328
|
`SDM_RATE_LIMIT_ENABLED=false` disables enforcement entirely. Use the in-memory backend for local development and the Redis backend for any deployment with more than one worker or instance that must enforce a single global limit.
|
|
329
|
+
|
|
330
|
+
By default, the API does not trust `X-Forwarded-For`; rate limiting and logs
|
|
331
|
+
use the direct peer address. Set `SDM_TRUSTED_PROXIES` to the proxy IPs/CIDRs
|
|
332
|
+
only when the direct peer is a trusted proxy, after which the rightmost
|
|
333
|
+
non-trusted address in the XFF chain is used. Deployments behind a proxy must
|
|
334
|
+
also configure `SDM_HEALTH_PROBE_TOKEN` for readiness probes; readiness never
|
|
335
|
+
uses XFF for authorization.
|
|
336
|
+
|
|
337
|
+
SQL-bearing POST responses and health probes use `Cache-Control: no-store`;
|
|
338
|
+
public catalog responses use short `private` caching. Full deployment details
|
|
339
|
+
will be documented in `docs/deployment.md`.
|
|
340
|
+
|
|
341
|
+
See [`docs/deployment.md`](docs/deployment.md) for production deployment guidance.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Resolve client IPs without trusting spoofable forwarding headers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ipaddress
|
|
6
|
+
import re
|
|
7
|
+
from collections.abc import Iterable
|
|
8
|
+
|
|
9
|
+
from fastapi import Request
|
|
10
|
+
|
|
11
|
+
_BRACKETED_IPV6 = re.compile(r"^\[([^\]]+)\](?::\d+)?$")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _parse_ip(value: str) -> ipaddress.IPv4Address | ipaddress.IPv6Address | None:
|
|
15
|
+
candidate = value.strip()
|
|
16
|
+
if not candidate or "%" in candidate:
|
|
17
|
+
return None
|
|
18
|
+
bracketed = _BRACKETED_IPV6.fullmatch(candidate)
|
|
19
|
+
if bracketed:
|
|
20
|
+
candidate = bracketed.group(1)
|
|
21
|
+
elif candidate.count(":") == 1:
|
|
22
|
+
host, port = candidate.rsplit(":", 1)
|
|
23
|
+
if port.isdigit():
|
|
24
|
+
candidate = host
|
|
25
|
+
try:
|
|
26
|
+
return ipaddress.ip_address(candidate)
|
|
27
|
+
except ValueError:
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _is_trusted(
|
|
32
|
+
address: ipaddress.IPv4Address | ipaddress.IPv6Address,
|
|
33
|
+
trusted_networks: Iterable,
|
|
34
|
+
) -> bool:
|
|
35
|
+
return any(
|
|
36
|
+
address.version == network.version and address in network
|
|
37
|
+
for network in trusted_networks
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def resolve_client_ip(request: Request, trusted_networks: tuple = ()) -> str:
|
|
42
|
+
"""Resolve the client address, trusting XFF only behind trusted peers."""
|
|
43
|
+
peer = request.client.host if request.client else "unknown"
|
|
44
|
+
if not trusted_networks or peer == "unknown":
|
|
45
|
+
return peer
|
|
46
|
+
|
|
47
|
+
peer_address = _parse_ip(peer)
|
|
48
|
+
if peer_address is None or not _is_trusted(peer_address, trusted_networks):
|
|
49
|
+
return peer
|
|
50
|
+
|
|
51
|
+
forwarded_for = request.headers.get("X-Forwarded-For", "")
|
|
52
|
+
for value in reversed(forwarded_for.split(",")):
|
|
53
|
+
address = _parse_ip(value)
|
|
54
|
+
if address is None or _is_trusted(address, trusted_networks):
|
|
55
|
+
continue
|
|
56
|
+
return str(address)
|
|
57
|
+
return peer
|
|
@@ -235,6 +235,7 @@ app = FastAPI(
|
|
|
235
235
|
|
|
236
236
|
# Import middleware
|
|
237
237
|
from backend.api.middleware import (
|
|
238
|
+
CacheControlMiddleware,
|
|
238
239
|
RateLimiter,
|
|
239
240
|
RateLimitMiddleware,
|
|
240
241
|
SecurityHeadersMiddleware,
|
|
@@ -244,6 +245,7 @@ from backend.core.exceptions import SDMException, UnsupportedDialectError, Error
|
|
|
244
245
|
|
|
245
246
|
# Configure CORS from the centralized Pydantic settings source.
|
|
246
247
|
ALLOWED_ORIGINS = [origin.strip() for origin in settings.allowed_origins.split(",") if origin.strip()]
|
|
248
|
+
TRUSTED_PROXY_NETWORKS = settings.trusted_proxy_networks
|
|
247
249
|
|
|
248
250
|
app.add_middleware(
|
|
249
251
|
CORSMiddleware,
|
|
@@ -285,10 +287,15 @@ app.add_middleware(
|
|
|
285
287
|
RateLimitMiddleware,
|
|
286
288
|
limiter=rate_limiter,
|
|
287
289
|
enabled=settings.rate_limit_enabled,
|
|
290
|
+
trusted_proxies=TRUSTED_PROXY_NETWORKS,
|
|
288
291
|
)
|
|
289
292
|
|
|
290
293
|
# Structured request logging must wrap rate limiting so rejected requests also get IDs.
|
|
291
|
-
app.add_middleware(
|
|
294
|
+
app.add_middleware(
|
|
295
|
+
StructuredLoggingMiddleware,
|
|
296
|
+
trusted_proxies=TRUSTED_PROXY_NETWORKS,
|
|
297
|
+
)
|
|
298
|
+
app.add_middleware(CacheControlMiddleware)
|
|
292
299
|
|
|
293
300
|
# Initialize services
|
|
294
301
|
transpiler = SQLTranspiler()
|
|
@@ -19,6 +19,7 @@ from starlette.concurrency import run_in_threadpool
|
|
|
19
19
|
|
|
20
20
|
from backend.core.exceptions import ErrorCode
|
|
21
21
|
|
|
22
|
+
from .client_identity import resolve_client_ip
|
|
22
23
|
from .rate_limit_store import InMemoryRateLimitStore, RateLimitStore, create_rate_limit_store
|
|
23
24
|
from .readiness import health_probe_response
|
|
24
25
|
|
|
@@ -35,7 +36,7 @@ def _sanitize_log_value(value: str, max_length: int = 500) -> str:
|
|
|
35
36
|
class RateLimitEntry:
|
|
36
37
|
"""Legacy compatibility entry."""
|
|
37
38
|
requests: int = 0
|
|
38
|
-
window_start: float = field(default_factory=time.
|
|
39
|
+
window_start: float = field(default_factory=time.monotonic)
|
|
39
40
|
|
|
40
41
|
|
|
41
42
|
class RateLimiter:
|
|
@@ -67,13 +68,14 @@ class RateLimiter:
|
|
|
67
68
|
class RateLimitMiddleware(BaseHTTPMiddleware):
|
|
68
69
|
"""Rate limiting middleware with a bounded request-body guard."""
|
|
69
70
|
|
|
70
|
-
def __init__(self, app, limiter: RateLimiter = None, enabled: bool = True, max_body_bytes: int = DEFAULT_MAX_REQUEST_BODY_BYTES):
|
|
71
|
+
def __init__(self, app, limiter: RateLimiter = None, enabled: bool = True, max_body_bytes: int = DEFAULT_MAX_REQUEST_BODY_BYTES, trusted_proxies: tuple = ()):
|
|
71
72
|
super().__init__(app)
|
|
72
73
|
if max_body_bytes <= 0:
|
|
73
74
|
raise ValueError("max_body_bytes must be positive")
|
|
74
75
|
self.limiter = limiter or RateLimiter(store=None if enabled else InMemoryRateLimitStore())
|
|
75
76
|
self.enabled = enabled
|
|
76
77
|
self.max_body_bytes = max_body_bytes
|
|
78
|
+
self.trusted_proxies = trusted_proxies
|
|
77
79
|
|
|
78
80
|
async def _read_bounded_body(self, request: Request) -> Response | None:
|
|
79
81
|
"""Consume and cache request bodies without trusting Content-Length."""
|
|
@@ -171,15 +173,16 @@ class RateLimitMiddleware(BaseHTTPMiddleware):
|
|
|
171
173
|
return response
|
|
172
174
|
|
|
173
175
|
def _get_client_id(self, request: Request) -> str:
|
|
174
|
-
return request
|
|
176
|
+
return resolve_client_ip(request, self.trusted_proxies)
|
|
175
177
|
|
|
176
178
|
|
|
177
179
|
class StructuredLoggingMiddleware(BaseHTTPMiddleware):
|
|
178
180
|
"""Structured request logging and request-id propagation."""
|
|
179
181
|
|
|
180
|
-
def __init__(self, app, log_body: bool = False):
|
|
182
|
+
def __init__(self, app, log_body: bool = False, trusted_proxies: tuple = ()):
|
|
181
183
|
super().__init__(app)
|
|
182
184
|
self.log_body = log_body
|
|
185
|
+
self.trusted_proxies = trusted_proxies
|
|
183
186
|
|
|
184
187
|
@staticmethod
|
|
185
188
|
def _get_request_id(request: Request) -> str:
|
|
@@ -346,7 +349,7 @@ class StructuredLoggingMiddleware(BaseHTTPMiddleware):
|
|
|
346
349
|
request.state.request_id = request_id
|
|
347
350
|
status_code = 500
|
|
348
351
|
error = None
|
|
349
|
-
log_data = {"event": "request_start", "request_id": request_id, "method": _sanitize_log_value(request.method), "path": _sanitize_log_value(request.url.path), "query": _sanitize_log_value(str(request.query_params)), "client": _sanitize_log_value(request
|
|
352
|
+
log_data = {"event": "request_start", "request_id": request_id, "method": _sanitize_log_value(request.method), "path": _sanitize_log_value(request.url.path), "query": _sanitize_log_value(str(request.query_params)), "client": _sanitize_log_value(resolve_client_ip(request, self.trusted_proxies)), "timestamp": datetime.now().isoformat()}
|
|
350
353
|
logger.info(json.dumps(log_data))
|
|
351
354
|
try:
|
|
352
355
|
response = await call_next(request)
|
|
@@ -370,6 +373,40 @@ class SecurityHeadersMiddleware(BaseHTTPMiddleware):
|
|
|
370
373
|
response = await call_next(request)
|
|
371
374
|
response.headers["X-Content-Type-Options"] = "nosniff"
|
|
372
375
|
response.headers["X-Frame-Options"] = "DENY"
|
|
373
|
-
response.headers["X-XSS-Protection"] = "
|
|
376
|
+
response.headers["X-XSS-Protection"] = "0"
|
|
374
377
|
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
|
|
375
378
|
return response
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
class CacheControlMiddleware(BaseHTTPMiddleware):
|
|
382
|
+
"""Apply explicit browser/proxy caching policy to API responses."""
|
|
383
|
+
|
|
384
|
+
@staticmethod
|
|
385
|
+
def _policy(request: Request) -> str | None:
|
|
386
|
+
if request.method == "OPTIONS":
|
|
387
|
+
return None
|
|
388
|
+
path = request.url.path
|
|
389
|
+
if path in {"/docs", "/redoc", "/openapi.json"}:
|
|
390
|
+
return None
|
|
391
|
+
if request.method == "POST" and path.startswith("/api/"):
|
|
392
|
+
return "no-store"
|
|
393
|
+
if request.method in {"GET", "HEAD"}:
|
|
394
|
+
if path in {"/health", "/ready", "/health/deep"}:
|
|
395
|
+
return "no-store"
|
|
396
|
+
if path == "/api/dialects":
|
|
397
|
+
return "private, max-age=60"
|
|
398
|
+
if path == "/api/functions" or path.startswith("/api/functions/"):
|
|
399
|
+
return "private, max-age=60"
|
|
400
|
+
if path == "/api/types" or path.startswith("/api/types/"):
|
|
401
|
+
return "private, max-age=60"
|
|
402
|
+
if path in {"/", "/api/stats"}:
|
|
403
|
+
return "private, max-age=15"
|
|
404
|
+
return None
|
|
405
|
+
|
|
406
|
+
async def dispatch(self, request: Request, call_next) -> Response:
|
|
407
|
+
response = await call_next(request)
|
|
408
|
+
if not response.headers.get("Cache-Control"):
|
|
409
|
+
policy = self._policy(request)
|
|
410
|
+
if policy:
|
|
411
|
+
response.headers["Cache-Control"] = policy
|
|
412
|
+
return response
|
|
@@ -44,7 +44,7 @@ class InMemoryRateLimitStore:
|
|
|
44
44
|
|
|
45
45
|
def check(self, key: str, limit: int, window_seconds: int) -> tuple[bool, int, int]:
|
|
46
46
|
with self._lock:
|
|
47
|
-
now = time.
|
|
47
|
+
now = time.monotonic()
|
|
48
48
|
self._prune_expired(now)
|
|
49
49
|
count, window_start = self._entries.get(key, (0, now))
|
|
50
50
|
if now - window_start >= window_seconds:
|
|
@@ -58,7 +58,7 @@ class InMemoryRateLimitStore:
|
|
|
58
58
|
|
|
59
59
|
def stats(self) -> dict:
|
|
60
60
|
with self._lock:
|
|
61
|
-
self._prune_expired(time.
|
|
61
|
+
self._prune_expired(time.monotonic())
|
|
62
62
|
return {"backend": "memory", "active_clients": len(self._entries)}
|
|
63
63
|
|
|
64
64
|
|
|
@@ -10,6 +10,7 @@ Supports configuration via:
|
|
|
10
10
|
- Default values
|
|
11
11
|
"""
|
|
12
12
|
import logging
|
|
13
|
+
import ipaddress
|
|
13
14
|
import sys
|
|
14
15
|
from enum import Enum
|
|
15
16
|
from typing import Any, Dict, List, Literal, Optional, TypedDict
|
|
@@ -225,6 +226,7 @@ class AppSettings(BaseSettings):
|
|
|
225
226
|
"http://localhost:5174,http://127.0.0.1:5174,"
|
|
226
227
|
"http://localhost:4174,http://127.0.0.1:4174"
|
|
227
228
|
)
|
|
229
|
+
trusted_proxies: str = ""
|
|
228
230
|
max_batch_size: int = Field(100, gt=0)
|
|
229
231
|
request_timeout: int = Field(30, gt=0)
|
|
230
232
|
|
|
@@ -299,6 +301,40 @@ class AppSettings(BaseSettings):
|
|
|
299
301
|
"log_level must be one of DEBUG, INFO, WARNING, ERROR, CRITICAL"
|
|
300
302
|
)
|
|
301
303
|
return normalized
|
|
304
|
+
|
|
305
|
+
@field_validator("trusted_proxies", mode="before")
|
|
306
|
+
@classmethod
|
|
307
|
+
def _normalize_trusted_proxies(cls, value: str) -> str:
|
|
308
|
+
if value is None:
|
|
309
|
+
return ""
|
|
310
|
+
if not isinstance(value, str):
|
|
311
|
+
raise ValueError("trusted_proxies must be a comma-separated string")
|
|
312
|
+
normalized = []
|
|
313
|
+
for raw_value in value.split(","):
|
|
314
|
+
candidate = raw_value.strip()
|
|
315
|
+
if not candidate:
|
|
316
|
+
continue
|
|
317
|
+
try:
|
|
318
|
+
network = ipaddress.ip_network(candidate, strict=False)
|
|
319
|
+
except ValueError as exc:
|
|
320
|
+
raise ValueError(
|
|
321
|
+
f"trusted_proxies contains an invalid IP/CIDR: {candidate!r}"
|
|
322
|
+
) from exc
|
|
323
|
+
if network.prefixlen == 0:
|
|
324
|
+
raise ValueError("trusted_proxies must not contain a catch-all network")
|
|
325
|
+
normalized.append(str(network))
|
|
326
|
+
return ",".join(normalized)
|
|
327
|
+
|
|
328
|
+
@property
|
|
329
|
+
def trusted_proxy_networks(self) -> tuple:
|
|
330
|
+
"""Return parsed trusted proxy networks for request identity checks."""
|
|
331
|
+
if not self.trusted_proxies:
|
|
332
|
+
return ()
|
|
333
|
+
return tuple(
|
|
334
|
+
ipaddress.ip_network(value, strict=False)
|
|
335
|
+
for value in self.trusted_proxies.split(",")
|
|
336
|
+
if value
|
|
337
|
+
)
|
|
302
338
|
|
|
303
339
|
def to_dict(self) -> Dict[str, Any]:
|
|
304
340
|
"""Convert settings to dictionary."""
|
|
@@ -17,8 +17,8 @@ Design rules:
|
|
|
17
17
|
from __future__ import annotations
|
|
18
18
|
|
|
19
19
|
# ── Release version facts (sole source of truth) ────────────────────────
|
|
20
|
-
PROJECT_VERSION = "1.1.
|
|
21
|
-
API_VERSION = "1.1.
|
|
20
|
+
PROJECT_VERSION = "1.1.7"
|
|
21
|
+
API_VERSION = "1.1.7"
|
|
22
22
|
# Frontend v2 architecture — independent of the product release version.
|
|
23
23
|
FRONTEND_ARCHITECTURE_VERSION = 2
|
|
24
24
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sql-dialect-master
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.7
|
|
4
4
|
Summary: SQL Dialect Master - Multi-database SQL conversion engine
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -35,7 +35,7 @@ Dynamic: license-file
|
|
|
35
35
|
|
|
36
36
|
Enterprise-grade multi-database SQL conversion engine supporting 12 database dialects.
|
|
37
37
|
|
|
38
|
-
Current release: 1.1.
|
|
38
|
+
Current release: 1.1.7
|
|
39
39
|
|
|
40
40
|
[](https://www.python.org/downloads/)
|
|
41
41
|
[](https://opensource.org/licenses/MIT)
|
|
@@ -359,3 +359,16 @@ Rate limiting is on by default. The two backends are:
|
|
|
359
359
|
- `SDM_RATE_LIMIT_BACKEND=redis` — shared, atomic limits across all workers. Set `SDM_REDIS_URL` when using this backend. The store connects and pings Redis at startup and fails fast if it is unreachable; the `redis` package is required (install with `pip install redis`).
|
|
360
360
|
|
|
361
361
|
`SDM_RATE_LIMIT_ENABLED=false` disables enforcement entirely. Use the in-memory backend for local development and the Redis backend for any deployment with more than one worker or instance that must enforce a single global limit.
|
|
362
|
+
|
|
363
|
+
By default, the API does not trust `X-Forwarded-For`; rate limiting and logs
|
|
364
|
+
use the direct peer address. Set `SDM_TRUSTED_PROXIES` to the proxy IPs/CIDRs
|
|
365
|
+
only when the direct peer is a trusted proxy, after which the rightmost
|
|
366
|
+
non-trusted address in the XFF chain is used. Deployments behind a proxy must
|
|
367
|
+
also configure `SDM_HEALTH_PROBE_TOKEN` for readiness probes; readiness never
|
|
368
|
+
uses XFF for authorization.
|
|
369
|
+
|
|
370
|
+
SQL-bearing POST responses and health probes use `Cache-Control: no-store`;
|
|
371
|
+
public catalog responses use short `private` caching. Full deployment details
|
|
372
|
+
will be documented in `docs/deployment.md`.
|
|
373
|
+
|
|
374
|
+
See [`docs/deployment.md`](docs/deployment.md) for production deployment guidance.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/column_hint_validation.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/nl2sql_components/__init__.py
RENAMED
|
File without changes
|
{sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/nl2sql_components/aggregations.py
RENAMED
|
File without changes
|
|
File without changes
|
{sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/nl2sql_components/core_extract.py
RENAMED
|
File without changes
|
{sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/nl2sql_components/evidence.py
RENAMED
|
File without changes
|
{sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/nl2sql_components/mappings.py
RENAMED
|
File without changes
|
{sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/nl2sql_components/relations.py
RENAMED
|
File without changes
|
{sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/nl2sql_components/templates.py
RENAMED
|
File without changes
|
{sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/nl2sql_components/tokenizer.py
RENAMED
|
File without changes
|
|
File without changes
|
{sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/nl2sql_pipeline/__init__.py
RENAMED
|
File without changes
|
{sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/nl2sql_pipeline/aggregations.py
RENAMED
|
File without changes
|
{sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/nl2sql_pipeline/clause_extract.py
RENAMED
|
File without changes
|
{sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/nl2sql_pipeline/dialect.py
RENAMED
|
File without changes
|
{sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/nl2sql_pipeline/intent.py
RENAMED
|
File without changes
|
|
File without changes
|
{sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/nl2sql_pipeline/models.py
RENAMED
|
File without changes
|
{sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/nl2sql_pipeline/orchestrator.py
RENAMED
|
File without changes
|
{sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/nl2sql_pipeline/predicates.py
RENAMED
|
File without changes
|
{sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/nl2sql_pipeline/renderer.py
RENAMED
|
File without changes
|
{sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/nl2sql_pipeline/resolution.py
RENAMED
|
File without changes
|
{sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/nl2sql_pipeline/suggestions.py
RENAMED
|
File without changes
|
{sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/nl2sql_pipeline/templates.py
RENAMED
|
File without changes
|
{sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/nl2sql_pipeline/validation.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/sql_dialect_master.egg-info/requires.txt
RENAMED
|
File without changes
|
{sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/sql_dialect_master.egg-info/top_level.txt
RENAMED
|
File without changes
|