api-dock 0.4.8__tar.gz → 0.4.9__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.
- {api_dock-0.4.8 → api_dock-0.4.9}/PKG-INFO +1 -1
- {api_dock-0.4.8 → api_dock-0.4.9}/api_dock/route_mapper.py +1 -1
- {api_dock-0.4.8 → api_dock-0.4.9}/api_dock.egg-info/PKG-INFO +1 -1
- {api_dock-0.4.8 → api_dock-0.4.9}/api_dock.egg-info/SOURCES.txt +1 -6
- {api_dock-0.4.8 → api_dock-0.4.9}/pyproject.toml +1 -1
- api_dock-0.4.8/tests/test_config_syntax.py +0 -61
- api_dock-0.4.8/tests/test_curl_fixes.py +0 -47
- api_dock-0.4.8/tests/test_restrictions.py +0 -108
- api_dock-0.4.8/tests/test_root_endpoint.py +0 -44
- api_dock-0.4.8/tests/test_sql_append.py +0 -451
- {api_dock-0.4.8 → api_dock-0.4.9}/LICENSE.md +0 -0
- {api_dock-0.4.8 → api_dock-0.4.9}/README.md +0 -0
- {api_dock-0.4.8 → api_dock-0.4.9}/api_dock/__init__.py +0 -0
- {api_dock-0.4.8 → api_dock-0.4.9}/api_dock/auth.py +0 -0
- {api_dock-0.4.8 → api_dock-0.4.9}/api_dock/cli.py +0 -0
- {api_dock-0.4.8 → api_dock-0.4.9}/api_dock/config.py +0 -0
- {api_dock-0.4.8 → api_dock-0.4.9}/api_dock/config_discovery.py +0 -0
- {api_dock-0.4.8 → api_dock-0.4.9}/api_dock/database_config.py +0 -0
- {api_dock-0.4.8 → api_dock-0.4.9}/api_dock/encryption.py +0 -0
- {api_dock-0.4.8 → api_dock-0.4.9}/api_dock/fast_api.py +0 -0
- {api_dock-0.4.8 → api_dock-0.4.9}/api_dock/flask_api.py +0 -0
- {api_dock-0.4.8 → api_dock-0.4.9}/api_dock/sql_builder.py +0 -0
- {api_dock-0.4.8 → api_dock-0.4.9}/api_dock/storage_auth.py +0 -0
- {api_dock-0.4.8 → api_dock-0.4.9}/api_dock.egg-info/dependency_links.txt +0 -0
- {api_dock-0.4.8 → api_dock-0.4.9}/api_dock.egg-info/entry_points.txt +0 -0
- {api_dock-0.4.8 → api_dock-0.4.9}/api_dock.egg-info/requires.txt +0 -0
- {api_dock-0.4.8 → api_dock-0.4.9}/api_dock.egg-info/top_level.txt +0 -0
- {api_dock-0.4.8 → api_dock-0.4.9}/config/config.yaml +0 -0
- {api_dock-0.4.8 → api_dock-0.4.9}/config/databases/db_example.yaml +0 -0
- {api_dock-0.4.8 → api_dock-0.4.9}/config/databases/test_users.yaml +0 -0
- {api_dock-0.4.8 → api_dock-0.4.9}/config/remotes/remote_with_allowed_routes.yaml +0 -0
- {api_dock-0.4.8 → api_dock-0.4.9}/config/remotes/remote_with_custom_mapping.yaml +0 -0
- {api_dock-0.4.8 → api_dock-0.4.9}/config/remotes/remote_with_restrictions.yaml +0 -0
- {api_dock-0.4.8 → api_dock-0.4.9}/config/remotes/remote_with_wildcards.yaml +0 -0
- {api_dock-0.4.8 → api_dock-0.4.9}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: api_dock
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.9
|
|
4
4
|
Summary: A flexible API gateway that allows you to proxy requests to multiple remote APIs and Databases
|
|
5
5
|
Author-email: Brookie Guzder-Williams <bguzder-williams@berkeley.edu>
|
|
6
6
|
License: BSd 3-clause
|
|
@@ -206,7 +206,7 @@ class RouteMapper:
|
|
|
206
206
|
# Configure redirect behavior based on settings
|
|
207
207
|
# Note: httpx automatically follows redirects but blocks HTTPS->HTTP downgrades for security
|
|
208
208
|
# The follow_protocol_downgrades setting is documented but may require manual redirect handling
|
|
209
|
-
follow_redirects =
|
|
209
|
+
follow_redirects = self.settings.get("follow_redirects", True)
|
|
210
210
|
|
|
211
211
|
async with httpx.AsyncClient(follow_redirects=follow_redirects) as client:
|
|
212
212
|
try:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: api_dock
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.9
|
|
4
4
|
Summary: A flexible API gateway that allows you to proxy requests to multiple remote APIs and Databases
|
|
5
5
|
Author-email: Brookie Guzder-Williams <bguzder-williams@berkeley.edu>
|
|
6
6
|
License: BSd 3-clause
|
|
@@ -26,9 +26,4 @@ config/databases/test_users.yaml
|
|
|
26
26
|
config/remotes/remote_with_allowed_routes.yaml
|
|
27
27
|
config/remotes/remote_with_custom_mapping.yaml
|
|
28
28
|
config/remotes/remote_with_restrictions.yaml
|
|
29
|
-
config/remotes/remote_with_wildcards.yaml
|
|
30
|
-
tests/test_config_syntax.py
|
|
31
|
-
tests/test_curl_fixes.py
|
|
32
|
-
tests/test_restrictions.py
|
|
33
|
-
tests/test_root_endpoint.py
|
|
34
|
-
tests/test_sql_append.py
|
|
29
|
+
config/remotes/remote_with_wildcards.yaml
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "api_dock"
|
|
7
|
-
version = "0.4.
|
|
7
|
+
version = "0.4.9"
|
|
8
8
|
description = "A flexible API gateway that allows you to proxy requests to multiple remote APIs and Databases"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "BSd 3-clause"}
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
Test the updated config syntax
|
|
4
|
-
|
|
5
|
-
License: BSD 3-Clause
|
|
6
|
-
"""
|
|
7
|
-
import sys
|
|
8
|
-
sys.path.insert(0, '/workspace/api_dock')
|
|
9
|
-
|
|
10
|
-
from api_dock.config import _route_matches_pattern
|
|
11
|
-
|
|
12
|
-
def test_updated_syntax():
|
|
13
|
-
"""Test the updated route pattern matching with named variables."""
|
|
14
|
-
print("Testing Updated Route Pattern Syntax")
|
|
15
|
-
print("=" * 50)
|
|
16
|
-
|
|
17
|
-
# Test cases with the new syntax
|
|
18
|
-
tests = [
|
|
19
|
-
# Basic patterns
|
|
20
|
-
("users", "users", True),
|
|
21
|
-
("users/123", "users/{{user_id}}", True),
|
|
22
|
-
("users/abc", "users/{{user_id}}", True),
|
|
23
|
-
("users/123/profile", "users/{{user_id}}/profile", True),
|
|
24
|
-
("users/123/delete", "users/{{user_id}}/delete", True),
|
|
25
|
-
|
|
26
|
-
# Different variable names should still work
|
|
27
|
-
("posts/456", "posts/{{post_id}}", True),
|
|
28
|
-
("admin/789", "admin/{{admin_id}}", True),
|
|
29
|
-
|
|
30
|
-
# Non-matches
|
|
31
|
-
("users", "posts", False),
|
|
32
|
-
("users/123", "users", False),
|
|
33
|
-
("users/123/profile", "users/{{user_id}}/settings", False),
|
|
34
|
-
|
|
35
|
-
# Anonymous variables (should still work)
|
|
36
|
-
("users/123", "users/{{}}", True),
|
|
37
|
-
("posts/456", "posts/{{}}", True),
|
|
38
|
-
]
|
|
39
|
-
|
|
40
|
-
passed = 0
|
|
41
|
-
failed = 0
|
|
42
|
-
|
|
43
|
-
for route, pattern, expected in tests:
|
|
44
|
-
result = _route_matches_pattern(route, pattern)
|
|
45
|
-
status = "✅" if result == expected else "❌"
|
|
46
|
-
print(f"{status} '{route}' vs '{pattern}': {result} (expected {expected})")
|
|
47
|
-
|
|
48
|
-
if result == expected:
|
|
49
|
-
passed += 1
|
|
50
|
-
else:
|
|
51
|
-
failed += 1
|
|
52
|
-
|
|
53
|
-
print(f"\nResults: {passed} passed, {failed} failed")
|
|
54
|
-
|
|
55
|
-
if failed == 0:
|
|
56
|
-
print("🎉 All pattern matching tests passed!")
|
|
57
|
-
else:
|
|
58
|
-
print("⚠️ Some tests failed - check pattern matching logic")
|
|
59
|
-
|
|
60
|
-
if __name__ == "__main__":
|
|
61
|
-
test_updated_syntax()
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
Test the API Dock fixes
|
|
4
|
-
|
|
5
|
-
License: BSD 3-Clause
|
|
6
|
-
"""
|
|
7
|
-
import sys
|
|
8
|
-
sys.path.insert(0, '/workspace/api_dock')
|
|
9
|
-
|
|
10
|
-
from api_dock.config import _route_matches_pattern
|
|
11
|
-
|
|
12
|
-
def test_pattern_matching():
|
|
13
|
-
"""Test the fixed pattern matching function."""
|
|
14
|
-
print("Testing Pattern Matching Fixes")
|
|
15
|
-
print("=" * 40)
|
|
16
|
-
|
|
17
|
-
# Test with valid string patterns
|
|
18
|
-
tests = [
|
|
19
|
-
("", "", True), # Empty route should match empty pattern
|
|
20
|
-
("users", "users", True), # Exact match
|
|
21
|
-
("users/123", "users/{{}}", True), # Wildcard match
|
|
22
|
-
("users/123/delete", "users/{{}}/delete", True), # Complex wildcard
|
|
23
|
-
]
|
|
24
|
-
|
|
25
|
-
for route, pattern, expected in tests:
|
|
26
|
-
result = _route_matches_pattern(route, pattern)
|
|
27
|
-
status = "✅" if result == expected else "❌"
|
|
28
|
-
print(f"{status} Route '{route}' vs Pattern '{pattern}': {result} (expected {expected})")
|
|
29
|
-
|
|
30
|
-
# Test with invalid pattern types (should not crash)
|
|
31
|
-
print("\nTesting invalid pattern types:")
|
|
32
|
-
invalid_patterns = [
|
|
33
|
-
{"key": "value"}, # dict
|
|
34
|
-
["item1", "item2"], # list
|
|
35
|
-
None, # None
|
|
36
|
-
123, # number
|
|
37
|
-
]
|
|
38
|
-
|
|
39
|
-
for pattern in invalid_patterns:
|
|
40
|
-
try:
|
|
41
|
-
result = _route_matches_pattern("users", pattern)
|
|
42
|
-
print(f"✅ Pattern {type(pattern).__name__} handled gracefully: {result}")
|
|
43
|
-
except Exception as e:
|
|
44
|
-
print(f"❌ Pattern {type(pattern).__name__} caused error: {e}")
|
|
45
|
-
|
|
46
|
-
if __name__ == "__main__":
|
|
47
|
-
test_pattern_matching()
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
Test script for route restrictions and allowed routes functionality.
|
|
4
|
-
|
|
5
|
-
License: BSD 3-Clause
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
#
|
|
9
|
-
# IMPORTS
|
|
10
|
-
#
|
|
11
|
-
import sys
|
|
12
|
-
from pathlib import Path
|
|
13
|
-
|
|
14
|
-
# Add the api_dock package to the path for testing
|
|
15
|
-
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
16
|
-
|
|
17
|
-
from api_dock.config import is_route_allowed, load_main_config
|
|
18
|
-
|
|
19
|
-
#
|
|
20
|
-
# CONSTANTS
|
|
21
|
-
#
|
|
22
|
-
CONFIG_PATH = "config/config.yaml"
|
|
23
|
-
|
|
24
|
-
#
|
|
25
|
-
# PUBLIC
|
|
26
|
-
#
|
|
27
|
-
def test_route_restrictions():
|
|
28
|
-
"""Test route restriction functionality."""
|
|
29
|
-
print("Testing route restrictions...")
|
|
30
|
-
|
|
31
|
-
# Load main config
|
|
32
|
-
try:
|
|
33
|
-
config = load_main_config(CONFIG_PATH)
|
|
34
|
-
print("✓ Config loaded successfully")
|
|
35
|
-
except Exception as e:
|
|
36
|
-
print(f"✗ Failed to load config: {e}")
|
|
37
|
-
return False
|
|
38
|
-
|
|
39
|
-
# Test cases: (route, remote, method, expected, description)
|
|
40
|
-
test_cases = [
|
|
41
|
-
# Global restrictions (should block users/{}/delete for all remotes)
|
|
42
|
-
("users/123/delete", "remote_with_restrictions", None, False, "Global restriction"),
|
|
43
|
-
("users/456/delete", "remote_with_allowed_routes", None, False, "Global restriction"),
|
|
44
|
-
|
|
45
|
-
# Remote-specific restrictions (remote_with_restrictions blocks users/{}/permissions)
|
|
46
|
-
("users/123/permissions", "remote_with_restrictions", None, False, "Remote-specific restriction"),
|
|
47
|
-
("users/123/permissions", "remote_with_allowed_routes", None, True, "Not restricted on this remote"),
|
|
48
|
-
|
|
49
|
-
# Allowed routes (remote_with_allowed_routes only allows specific patterns)
|
|
50
|
-
("users", "remote_with_allowed_routes", None, True, "Explicitly allowed"),
|
|
51
|
-
("users/123", "remote_with_allowed_routes", None, True, "Explicitly allowed"),
|
|
52
|
-
("users/123/profile", "remote_with_allowed_routes", None, True, "Explicitly allowed"),
|
|
53
|
-
("users/123/settings", "remote_with_allowed_routes", None, False, "Not in allowed list"),
|
|
54
|
-
("admin/dashboard", "remote_with_allowed_routes", None, False, "Not in allowed list"),
|
|
55
|
-
|
|
56
|
-
# Custom mapping remote (should respect restrictions)
|
|
57
|
-
("users/123/permissions", "remote_with_custom_mapping", None, True, "Should be allowed"),
|
|
58
|
-
("users/123/delete", "remote_with_custom_mapping", None, False, "Should be restricted"),
|
|
59
|
-
|
|
60
|
-
# Admin routes (should be blocked for remote_with_restrictions only)
|
|
61
|
-
("admin/dashboard", "remote_with_restrictions", None, False, "Admin routes restricted"),
|
|
62
|
-
("admin/dashboard", "remote_with_custom_mapping", None, True, "Admin not restricted here"),
|
|
63
|
-
|
|
64
|
-
# Wildcard tests - * for single segment
|
|
65
|
-
("users/123/delete", "remote_with_restrictions", None, False, "Wildcard pattern match"),
|
|
66
|
-
("admin/settings", "remote_with_restrictions", None, False, "Wildcard admin/* pattern"),
|
|
67
|
-
|
|
68
|
-
# Method-aware restrictions (if config supports them)
|
|
69
|
-
("api/test", "remote_with_restrictions", "GET", True, "GET should be allowed"),
|
|
70
|
-
("api/test", "remote_with_restrictions", "DELETE", True, "DELETE allowed (no method restriction)"),
|
|
71
|
-
]
|
|
72
|
-
|
|
73
|
-
passed = 0
|
|
74
|
-
failed = 0
|
|
75
|
-
|
|
76
|
-
for route, remote, method, expected, description in test_cases:
|
|
77
|
-
result = is_route_allowed(route, config, remote, method=method)
|
|
78
|
-
if result == expected:
|
|
79
|
-
method_str = f" [{method}]" if method else ""
|
|
80
|
-
print(f"✓ {route}{method_str} on {remote}: {result} ({description})")
|
|
81
|
-
passed += 1
|
|
82
|
-
else:
|
|
83
|
-
method_str = f" [{method}]" if method else ""
|
|
84
|
-
print(f"✗ {route}{method_str} on {remote}: expected {expected}, got {result} ({description})")
|
|
85
|
-
failed += 1
|
|
86
|
-
|
|
87
|
-
print(f"\nResults: {passed} passed, {failed} failed")
|
|
88
|
-
return failed == 0
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
def main():
|
|
92
|
-
"""Run all tests."""
|
|
93
|
-
print("=" * 60)
|
|
94
|
-
print("API Dock Route Restrictions Test")
|
|
95
|
-
print("=" * 60)
|
|
96
|
-
|
|
97
|
-
success = test_route_restrictions()
|
|
98
|
-
|
|
99
|
-
if success:
|
|
100
|
-
print("\n✓ All tests passed!")
|
|
101
|
-
return 0
|
|
102
|
-
else:
|
|
103
|
-
print("\n✗ Some tests failed!")
|
|
104
|
-
return 1
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
if __name__ == "__main__":
|
|
108
|
-
sys.exit(main())
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
Test script for API Dock root endpoint
|
|
4
|
-
|
|
5
|
-
License: BSD 3-Clause
|
|
6
|
-
"""
|
|
7
|
-
import sys
|
|
8
|
-
sys.path.insert(0, '/workspace/api_dock')
|
|
9
|
-
|
|
10
|
-
from api_dock.route_mapper import RouteMapper
|
|
11
|
-
|
|
12
|
-
def test_api_dock_metadata():
|
|
13
|
-
"""Test the enhanced metadata returned by the root endpoint."""
|
|
14
|
-
print("Testing API Dock Enhanced Metadata")
|
|
15
|
-
print("=" * 50)
|
|
16
|
-
|
|
17
|
-
# Test with the main api_dock config
|
|
18
|
-
print("\n1. Main API Dock Config:")
|
|
19
|
-
print("-" * 30)
|
|
20
|
-
route_mapper = RouteMapper("/workspace/api_dock/config/config.yaml")
|
|
21
|
-
metadata = route_mapper.get_config_metadata()
|
|
22
|
-
|
|
23
|
-
for key, value in metadata.items():
|
|
24
|
-
print(f"{key}: {value}")
|
|
25
|
-
|
|
26
|
-
# Test with the test project config
|
|
27
|
-
print("\n2. Test Project Config:")
|
|
28
|
-
print("-" * 30)
|
|
29
|
-
route_mapper_test = RouteMapper("/workspace/api_dock_test_project/api_dock_config/config.yaml")
|
|
30
|
-
metadata_test = route_mapper_test.get_config_metadata()
|
|
31
|
-
|
|
32
|
-
for key, value in metadata_test.items():
|
|
33
|
-
print(f"{key}: {value}")
|
|
34
|
-
|
|
35
|
-
print("\n" + "=" * 50)
|
|
36
|
-
print("Expected structure:")
|
|
37
|
-
print("- name: API name")
|
|
38
|
-
print("- description: API description")
|
|
39
|
-
print("- authors: List of authors")
|
|
40
|
-
print("- endpoints: List of non-remote endpoints")
|
|
41
|
-
print("- remotes: List of remote API names")
|
|
42
|
-
|
|
43
|
-
if __name__ == "__main__":
|
|
44
|
-
test_api_dock_metadata()
|
|
@@ -1,451 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
Tests for sql_append functionality in sql_builder.
|
|
4
|
-
|
|
5
|
-
Tests cover:
|
|
6
|
-
- build_append_clause_from_params: basic, defaults, optional, cross-param refs
|
|
7
|
-
- build_sql_query integration: WHERE + append combined
|
|
8
|
-
- _substitute_variables_raw: unquoted substitution
|
|
9
|
-
- _sanitize_sql_identifier: injection prevention
|
|
10
|
-
- _apply_default_values: value-only params
|
|
11
|
-
- build_where_clause_from_params: skips sql_append and value-only params
|
|
12
|
-
- validate_route_config: accepts sql_append
|
|
13
|
-
|
|
14
|
-
License: BSD 3-Clause
|
|
15
|
-
"""
|
|
16
|
-
import sys
|
|
17
|
-
sys.path.insert(0, '/workspace/api_dock')
|
|
18
|
-
|
|
19
|
-
from api_dock.sql_builder import (
|
|
20
|
-
build_append_clause_from_params,
|
|
21
|
-
build_where_clause_from_params,
|
|
22
|
-
build_sql_query,
|
|
23
|
-
_substitute_variables_raw,
|
|
24
|
-
_sanitize_sql_identifier,
|
|
25
|
-
_apply_default_values,
|
|
26
|
-
)
|
|
27
|
-
from api_dock.database_config import validate_route_config, merge_query_params
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
SIMPLE_DB_CONFIG = {
|
|
31
|
-
"tables": {
|
|
32
|
-
"users": "test/users.parquet",
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
passed = 0
|
|
38
|
-
failed = 0
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def check(test_name, actual, expected):
|
|
42
|
-
global passed, failed
|
|
43
|
-
if actual == expected:
|
|
44
|
-
print(f" ✅ {test_name}")
|
|
45
|
-
passed += 1
|
|
46
|
-
else:
|
|
47
|
-
print(f" ❌ {test_name}")
|
|
48
|
-
print(f" expected: {expected!r}")
|
|
49
|
-
print(f" actual: {actual!r}")
|
|
50
|
-
failed += 1
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def check_raises(test_name, func, *args, exc_type=ValueError):
|
|
54
|
-
global passed, failed
|
|
55
|
-
try:
|
|
56
|
-
func(*args)
|
|
57
|
-
print(f" ❌ {test_name} (no exception raised)")
|
|
58
|
-
failed += 1
|
|
59
|
-
except exc_type:
|
|
60
|
-
print(f" ✅ {test_name}")
|
|
61
|
-
passed += 1
|
|
62
|
-
except Exception as e:
|
|
63
|
-
print(f" ❌ {test_name} (wrong exception: {type(e).__name__}: {e})")
|
|
64
|
-
failed += 1
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def check_in(test_name, haystack, needle):
|
|
68
|
-
global passed, failed
|
|
69
|
-
if needle in haystack:
|
|
70
|
-
print(f" ✅ {test_name}")
|
|
71
|
-
passed += 1
|
|
72
|
-
else:
|
|
73
|
-
print(f" ❌ {test_name}")
|
|
74
|
-
print(f" '{needle}' not found in '{haystack}'")
|
|
75
|
-
failed += 1
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
def check_not_in(test_name, haystack, needle):
|
|
79
|
-
global passed, failed
|
|
80
|
-
if needle not in haystack:
|
|
81
|
-
print(f" ✅ {test_name}")
|
|
82
|
-
passed += 1
|
|
83
|
-
else:
|
|
84
|
-
print(f" ❌ {test_name}")
|
|
85
|
-
print(f" '{needle}' unexpectedly found in '{haystack}'")
|
|
86
|
-
failed += 1
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
# ===========================================================================
|
|
90
|
-
print("\n_sanitize_sql_identifier")
|
|
91
|
-
print("=" * 50)
|
|
92
|
-
|
|
93
|
-
check("simple column name", _sanitize_sql_identifier("name"), "name")
|
|
94
|
-
check("qualified column", _sanitize_sql_identifier("u.created_date"), "u.created_date")
|
|
95
|
-
check("ASC keyword", _sanitize_sql_identifier("ASC"), "ASC")
|
|
96
|
-
check("DESC keyword", _sanitize_sql_identifier("DESC"), "DESC")
|
|
97
|
-
check("integer string", _sanitize_sql_identifier("100"), "100")
|
|
98
|
-
check_raises("rejects semicolon", _sanitize_sql_identifier, "name; DROP TABLE users")
|
|
99
|
-
check_raises("rejects quotes", _sanitize_sql_identifier, "name' OR '1'='1")
|
|
100
|
-
check_raises("rejects double dash comment", _sanitize_sql_identifier, "name -- comment")
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
# ===========================================================================
|
|
104
|
-
print("\n_substitute_variables_raw")
|
|
105
|
-
print("=" * 50)
|
|
106
|
-
|
|
107
|
-
result = _substitute_variables_raw("ORDER BY {{col}}", {"col": "name"})
|
|
108
|
-
check("basic substitution", result, "ORDER BY name")
|
|
109
|
-
|
|
110
|
-
result = _substitute_variables_raw("LIMIT {{limit}}", {"limit": "10"})
|
|
111
|
-
check("no quoting", result, "LIMIT 10")
|
|
112
|
-
check("no single quotes in result", "'" in result, False)
|
|
113
|
-
|
|
114
|
-
result = _substitute_variables_raw("ORDER BY {{sort}} {{dir}}", {"sort": "age", "dir": "ASC"})
|
|
115
|
-
check("multiple placeholders", result, "ORDER BY age ASC")
|
|
116
|
-
|
|
117
|
-
result = _substitute_variables_raw("LIMIT {{limit}}", {})
|
|
118
|
-
check("missing param left as placeholder", result, "LIMIT {{limit}}")
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
# ===========================================================================
|
|
122
|
-
print("\n_apply_default_values")
|
|
123
|
-
print("=" * 50)
|
|
124
|
-
|
|
125
|
-
route = {"query_params": [{"sort_direction": {"default": "DESC"}}]}
|
|
126
|
-
result = _apply_default_values(route, {"sort": "name"})
|
|
127
|
-
check("applies missing defaults", result, {"sort": "name", "sort_direction": "DESC"})
|
|
128
|
-
|
|
129
|
-
result = _apply_default_values(route, {"sort_direction": "ASC"})
|
|
130
|
-
check("does not override provided", result, {"sort_direction": "ASC"})
|
|
131
|
-
|
|
132
|
-
result = _apply_default_values({}, {"a": "1"})
|
|
133
|
-
check("no query_params section", result, {"a": "1"})
|
|
134
|
-
|
|
135
|
-
route = {"query_params": [{"limit": {"sql_append": "LIMIT {{limit}}", "default": 50}}]}
|
|
136
|
-
result = _apply_default_values(route, {})
|
|
137
|
-
check("converts default to string", result, {"limit": "50"})
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
# ===========================================================================
|
|
141
|
-
print("\nbuild_append_clause_from_params")
|
|
142
|
-
print("=" * 50)
|
|
143
|
-
|
|
144
|
-
# basic with default
|
|
145
|
-
route = {"query_params": [{"limit": {"sql_append": "LIMIT {{limit}}", "default": 100}}]}
|
|
146
|
-
result = build_append_clause_from_params(route, {}, {})
|
|
147
|
-
check("default kicks in", result, ["LIMIT 100"])
|
|
148
|
-
|
|
149
|
-
# override default
|
|
150
|
-
result = build_append_clause_from_params(route, {"limit": "25"}, {})
|
|
151
|
-
check("override default", result, ["LIMIT 25"])
|
|
152
|
-
|
|
153
|
-
# optional not provided
|
|
154
|
-
route = {"query_params": [{"offset": {"sql_append": "OFFSET {{offset}}"}}]}
|
|
155
|
-
result = build_append_clause_from_params(route, {}, {})
|
|
156
|
-
check("optional not provided", result, [])
|
|
157
|
-
|
|
158
|
-
# optional provided
|
|
159
|
-
result = build_append_clause_from_params(route, {"offset": "20"}, {})
|
|
160
|
-
check("optional provided", result, ["OFFSET 20"])
|
|
161
|
-
|
|
162
|
-
# ordering preserved
|
|
163
|
-
route = {
|
|
164
|
-
"query_params": [
|
|
165
|
-
{"sort": {"sql_append": "ORDER BY {{sort}}", "default": "id"}},
|
|
166
|
-
{"limit": {"sql_append": "LIMIT {{limit}}", "default": 50}},
|
|
167
|
-
{"offset": {"sql_append": "OFFSET {{offset}}"}},
|
|
168
|
-
]
|
|
169
|
-
}
|
|
170
|
-
result = build_append_clause_from_params(route, {"offset": "10"}, {})
|
|
171
|
-
check("ordering preserved", result, ["ORDER BY id", "LIMIT 50", "OFFSET 10"])
|
|
172
|
-
|
|
173
|
-
# cross-param variable reference (both defaults)
|
|
174
|
-
route = {
|
|
175
|
-
"query_params": [
|
|
176
|
-
{"sort": {"sql_append": "ORDER BY {{sort}} {{sort_direction}}", "default": "created_date"}},
|
|
177
|
-
{"sort_direction": {"default": "DESC"}},
|
|
178
|
-
]
|
|
179
|
-
}
|
|
180
|
-
result = build_append_clause_from_params(route, {}, {})
|
|
181
|
-
check("cross-param defaults", result, ["ORDER BY created_date DESC"])
|
|
182
|
-
|
|
183
|
-
# cross-param with overrides
|
|
184
|
-
result = build_append_clause_from_params(route, {"sort": "name", "sort_direction": "ASC"}, {})
|
|
185
|
-
check("cross-param overrides", result, ["ORDER BY name ASC"])
|
|
186
|
-
|
|
187
|
-
# skips sql params
|
|
188
|
-
route = {
|
|
189
|
-
"query_params": [
|
|
190
|
-
{"age": {"sql": "age = {{age}}"}},
|
|
191
|
-
{"limit": {"sql_append": "LIMIT {{limit}}", "default": 10}},
|
|
192
|
-
]
|
|
193
|
-
}
|
|
194
|
-
result = build_append_clause_from_params(route, {"age": "25"}, {})
|
|
195
|
-
check("skips sql params", result, ["LIMIT 10"])
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
# ===========================================================================
|
|
199
|
-
print("\nbuild_where_clause_from_params — skips sql_append and value-only")
|
|
200
|
-
print("=" * 50)
|
|
201
|
-
|
|
202
|
-
route = {
|
|
203
|
-
"query_params": [
|
|
204
|
-
{"age": {"sql": "age = {{age}}"}},
|
|
205
|
-
{"limit": {"sql_append": "LIMIT {{limit}}", "default": 10}},
|
|
206
|
-
]
|
|
207
|
-
}
|
|
208
|
-
result = build_where_clause_from_params(route, {"age": "25"}, {})
|
|
209
|
-
check("skips sql_append params (length)", len(result), 1)
|
|
210
|
-
check_in("only sql param present", result[0], "age")
|
|
211
|
-
|
|
212
|
-
route = {
|
|
213
|
-
"query_params": [
|
|
214
|
-
{"age": {"sql": "age = {{age}}"}},
|
|
215
|
-
{"sort_direction": {"default": "DESC"}},
|
|
216
|
-
]
|
|
217
|
-
}
|
|
218
|
-
result = build_where_clause_from_params(route, {"age": "30"}, {})
|
|
219
|
-
check("skips value-only params (length)", len(result), 1)
|
|
220
|
-
check_in("only sql param present (2)", result[0], "age")
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
# ===========================================================================
|
|
224
|
-
print("\nbuild_sql_query integration — WHERE + append combined")
|
|
225
|
-
print("=" * 50)
|
|
226
|
-
|
|
227
|
-
# WHERE and append combined
|
|
228
|
-
route = {
|
|
229
|
-
"sql": "SELECT * FROM [[users]]",
|
|
230
|
-
"query_params": [
|
|
231
|
-
{"age": {"sql": "age = {{age}}"}},
|
|
232
|
-
{"sort": {"sql_append": "ORDER BY {{sort}}", "default": "id"}},
|
|
233
|
-
{"limit": {"sql_append": "LIMIT {{limit}}", "default": 100}},
|
|
234
|
-
]
|
|
235
|
-
}
|
|
236
|
-
result = build_sql_query(route, SIMPLE_DB_CONFIG, path_params={}, query_params={"age": "25"})
|
|
237
|
-
check_in("has WHERE", result, "WHERE age = 25")
|
|
238
|
-
check_in("has ORDER BY", result, "ORDER BY id")
|
|
239
|
-
check_in("has LIMIT", result, "LIMIT 100")
|
|
240
|
-
where_pos = result.index("WHERE")
|
|
241
|
-
order_pos = result.index("ORDER BY")
|
|
242
|
-
limit_pos = result.index("LIMIT")
|
|
243
|
-
check("WHERE < ORDER BY < LIMIT", where_pos < order_pos < limit_pos, True)
|
|
244
|
-
|
|
245
|
-
# append only, no WHERE
|
|
246
|
-
route = {
|
|
247
|
-
"sql": "SELECT * FROM [[users]]",
|
|
248
|
-
"query_params": [
|
|
249
|
-
{"sort": {"sql_append": "ORDER BY {{sort}}", "default": "name"}},
|
|
250
|
-
{"limit": {"sql_append": "LIMIT {{limit}}", "default": 10}},
|
|
251
|
-
]
|
|
252
|
-
}
|
|
253
|
-
result = build_sql_query(route, SIMPLE_DB_CONFIG, path_params={}, query_params={})
|
|
254
|
-
check_not_in("no WHERE", result, "WHERE")
|
|
255
|
-
check_in("has ORDER BY (no where)", result, "ORDER BY name")
|
|
256
|
-
check_in("has LIMIT (no where)", result, "LIMIT 10")
|
|
257
|
-
|
|
258
|
-
# no query_params at all — backward compat
|
|
259
|
-
route = {"sql": "SELECT * FROM [[users]]"}
|
|
260
|
-
result = build_sql_query(route, SIMPLE_DB_CONFIG, path_params={}, query_params={})
|
|
261
|
-
check_in("backward compat has SELECT", result, "SELECT * FROM")
|
|
262
|
-
check_not_in("backward compat no WHERE", result, "WHERE")
|
|
263
|
-
check_not_in("backward compat no ORDER BY", result, "ORDER BY")
|
|
264
|
-
|
|
265
|
-
# sort with direction cross-param — overrides
|
|
266
|
-
route = {
|
|
267
|
-
"sql": "SELECT * FROM [[users]]",
|
|
268
|
-
"query_params": [
|
|
269
|
-
{"sort": {"sql_append": "ORDER BY {{sort}} {{sort_direction}}", "default": "created_date"}},
|
|
270
|
-
{"sort_direction": {"default": "DESC"}},
|
|
271
|
-
{"limit": {"sql_append": "LIMIT {{limit}}", "default": 50}},
|
|
272
|
-
]
|
|
273
|
-
}
|
|
274
|
-
result = build_sql_query(
|
|
275
|
-
route, SIMPLE_DB_CONFIG, path_params={},
|
|
276
|
-
query_params={"sort": "age", "sort_direction": "ASC", "limit": "20"}
|
|
277
|
-
)
|
|
278
|
-
check_in("sort+dir override", result, "ORDER BY age ASC")
|
|
279
|
-
check_in("limit override", result, "LIMIT 20")
|
|
280
|
-
|
|
281
|
-
# sort with direction cross-param — all defaults
|
|
282
|
-
result = build_sql_query(route, SIMPLE_DB_CONFIG, path_params={}, query_params={})
|
|
283
|
-
check_in("sort+dir defaults", result, "ORDER BY created_date DESC")
|
|
284
|
-
|
|
285
|
-
# existing WHERE in base SQL + append
|
|
286
|
-
route = {
|
|
287
|
-
"sql": "SELECT * FROM [[users]] WHERE active = true",
|
|
288
|
-
"query_params": [
|
|
289
|
-
{"age": {"sql": "age = {{age}}"}},
|
|
290
|
-
{"limit": {"sql_append": "LIMIT {{limit}}", "default": 10}},
|
|
291
|
-
]
|
|
292
|
-
}
|
|
293
|
-
result = build_sql_query(route, SIMPLE_DB_CONFIG, path_params={}, query_params={"age": "30"})
|
|
294
|
-
check_in("existing WHERE gets AND", result, "WHERE active = true AND age = 30")
|
|
295
|
-
check("ends with LIMIT 10", result.strip().endswith("LIMIT 10"), True)
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
# ===========================================================================
|
|
299
|
-
print("\nvalidate_route_config — accepts sql_append")
|
|
300
|
-
print("=" * 50)
|
|
301
|
-
|
|
302
|
-
config = {
|
|
303
|
-
"route": "users",
|
|
304
|
-
"sql": "SELECT * FROM users",
|
|
305
|
-
"query_params": [
|
|
306
|
-
{"limit": {"sql_append": "LIMIT {{limit}}", "default": 10}},
|
|
307
|
-
]
|
|
308
|
-
}
|
|
309
|
-
check("accepts sql_append", validate_route_config(config), True)
|
|
310
|
-
|
|
311
|
-
config = {
|
|
312
|
-
"route": "users",
|
|
313
|
-
"sql": "SELECT * FROM users",
|
|
314
|
-
"query_params": [
|
|
315
|
-
{"sort_direction": {"default": "DESC"}},
|
|
316
|
-
]
|
|
317
|
-
}
|
|
318
|
-
check("accepts value-only param", validate_route_config(config), True)
|
|
319
|
-
|
|
320
|
-
config = {
|
|
321
|
-
"route": "users",
|
|
322
|
-
"sql": "SELECT * FROM users",
|
|
323
|
-
"query_params": [
|
|
324
|
-
{"age": {"sql": "age = {{age}}"}},
|
|
325
|
-
{"sort": {"sql_append": "ORDER BY {{sort}}", "default": "id"}},
|
|
326
|
-
{"sort_direction": {"default": "DESC"}},
|
|
327
|
-
{"limit": {"sql_append": "LIMIT {{limit}}", "default": 50}},
|
|
328
|
-
]
|
|
329
|
-
}
|
|
330
|
-
check("accepts mixed sql + sql_append + value-only", validate_route_config(config), True)
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
# ===========================================================================
|
|
334
|
-
print("\nmerge_query_params — top-level query_params")
|
|
335
|
-
print("=" * 50)
|
|
336
|
-
|
|
337
|
-
# no top-level — returns original
|
|
338
|
-
route = {"route": "users", "sql": "SELECT * FROM users", "query_params": [{"age": {"sql": "age = {{age}}"}}]}
|
|
339
|
-
db = {"tables": {"users": "test.parquet"}}
|
|
340
|
-
merged = merge_query_params(route, db)
|
|
341
|
-
check("no top-level returns original", merged is route, True)
|
|
342
|
-
|
|
343
|
-
# top-level applied when route has no query_params
|
|
344
|
-
route = {"route": "users", "sql": "SELECT * FROM users"}
|
|
345
|
-
db = {"tables": {"users": "test.parquet"}, "query_params": [{"limit": {"sql_append": "LIMIT {{limit}}", "default": 10}}]}
|
|
346
|
-
merged = merge_query_params(route, db)
|
|
347
|
-
check("top-level applied to bare route (length)", len(merged["query_params"]), 1)
|
|
348
|
-
check("top-level applied to bare route (name)", next(iter(merged["query_params"][0])), "limit")
|
|
349
|
-
|
|
350
|
-
# top-level appended after route params
|
|
351
|
-
route = {
|
|
352
|
-
"route": "users", "sql": "SELECT * FROM users",
|
|
353
|
-
"query_params": [{"sort": {"sql_append": "ORDER BY {{sort}}", "default": "id"}}]
|
|
354
|
-
}
|
|
355
|
-
db = {"tables": {"users": "test.parquet"}, "query_params": [{"limit": {"sql_append": "LIMIT {{limit}}", "default": 50}}]}
|
|
356
|
-
merged = merge_query_params(route, db)
|
|
357
|
-
check("appended after route params (length)", len(merged["query_params"]), 2)
|
|
358
|
-
check("route param first", next(iter(merged["query_params"][0])), "sort")
|
|
359
|
-
check("top-level param second", next(iter(merged["query_params"][1])), "limit")
|
|
360
|
-
|
|
361
|
-
# route overrides top-level with same name
|
|
362
|
-
route = {
|
|
363
|
-
"route": "users", "sql": "SELECT * FROM users",
|
|
364
|
-
"query_params": [{"limit": {"sql_append": "LIMIT {{limit}}", "default": 999}}]
|
|
365
|
-
}
|
|
366
|
-
db = {"tables": {"users": "test.parquet"}, "query_params": [{"limit": {"sql_append": "LIMIT {{limit}}", "default": 10}}]}
|
|
367
|
-
merged = merge_query_params(route, db)
|
|
368
|
-
check("override same name (length)", len(merged["query_params"]), 1)
|
|
369
|
-
check("override keeps route version", merged["query_params"][0]["limit"]["default"], 999)
|
|
370
|
-
|
|
371
|
-
# partial override — only matching names are skipped
|
|
372
|
-
route = {
|
|
373
|
-
"route": "users", "sql": "SELECT * FROM users",
|
|
374
|
-
"query_params": [{"limit": {"sql_append": "LIMIT {{limit}}", "default": 20}}]
|
|
375
|
-
}
|
|
376
|
-
db = {
|
|
377
|
-
"tables": {"users": "test.parquet"},
|
|
378
|
-
"query_params": [
|
|
379
|
-
{"limit": {"sql_append": "LIMIT {{limit}}", "default": 10}},
|
|
380
|
-
{"offset": {"sql_append": "OFFSET {{offset}}"}},
|
|
381
|
-
]
|
|
382
|
-
}
|
|
383
|
-
merged = merge_query_params(route, db)
|
|
384
|
-
check("partial override (length)", len(merged["query_params"]), 2)
|
|
385
|
-
check("partial override keeps route limit", merged["query_params"][0]["limit"]["default"], 20)
|
|
386
|
-
check("partial override adds offset", next(iter(merged["query_params"][1])), "offset")
|
|
387
|
-
|
|
388
|
-
# original route_config is not mutated
|
|
389
|
-
route = {"route": "users", "sql": "SELECT * FROM users", "query_params": [{"age": {"sql": "age = {{age}}"}}]}
|
|
390
|
-
db = {"tables": {"users": "test.parquet"}, "query_params": [{"limit": {"sql_append": "LIMIT {{limit}}", "default": 10}}]}
|
|
391
|
-
merged = merge_query_params(route, db)
|
|
392
|
-
check("original not mutated", len(route["query_params"]), 1)
|
|
393
|
-
check("merged has both", len(merged["query_params"]), 2)
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
# ===========================================================================
|
|
397
|
-
print("\nmerge_query_params — integration with build_sql_query")
|
|
398
|
-
print("=" * 50)
|
|
399
|
-
|
|
400
|
-
# top-level limit applied via build_sql_query
|
|
401
|
-
route = {"sql": "SELECT * FROM [[users]]"}
|
|
402
|
-
db = {"tables": {"users": "test/users.parquet"}, "query_params": [{"limit": {"sql_append": "LIMIT {{limit}}", "default": 25}}]}
|
|
403
|
-
merged = merge_query_params(route, db)
|
|
404
|
-
result = build_sql_query(merged, db, path_params={}, query_params={})
|
|
405
|
-
check_in("integration: has LIMIT from top-level", result, "LIMIT 25")
|
|
406
|
-
|
|
407
|
-
# route sort + top-level limit
|
|
408
|
-
route = {
|
|
409
|
-
"sql": "SELECT * FROM [[users]]",
|
|
410
|
-
"query_params": [{"sort": {"sql_append": "ORDER BY {{sort}}", "default": "name"}}]
|
|
411
|
-
}
|
|
412
|
-
db = {"tables": {"users": "test/users.parquet"}, "query_params": [{"limit": {"sql_append": "LIMIT {{limit}}", "default": 50}}]}
|
|
413
|
-
merged = merge_query_params(route, db)
|
|
414
|
-
result = build_sql_query(merged, db, path_params={}, query_params={})
|
|
415
|
-
check_in("integration: has ORDER BY from route", result, "ORDER BY name")
|
|
416
|
-
check_in("integration: has LIMIT from top-level", result, "LIMIT 50")
|
|
417
|
-
order_pos = result.index("ORDER BY")
|
|
418
|
-
limit_pos = result.index("LIMIT")
|
|
419
|
-
check("integration: ORDER BY before LIMIT", order_pos < limit_pos, True)
|
|
420
|
-
|
|
421
|
-
# route overrides top-level limit
|
|
422
|
-
route = {
|
|
423
|
-
"sql": "SELECT * FROM [[users]]",
|
|
424
|
-
"query_params": [{"limit": {"sql_append": "LIMIT {{limit}}", "default": 999}}]
|
|
425
|
-
}
|
|
426
|
-
db = {"tables": {"users": "test/users.parquet"}, "query_params": [{"limit": {"sql_append": "LIMIT {{limit}}", "default": 10}}]}
|
|
427
|
-
merged = merge_query_params(route, db)
|
|
428
|
-
result = build_sql_query(merged, db, path_params={}, query_params={})
|
|
429
|
-
check_in("integration: route limit overrides top-level", result, "LIMIT 999")
|
|
430
|
-
check_not_in("integration: top-level limit not present", result, "LIMIT 10")
|
|
431
|
-
|
|
432
|
-
# WHERE from route + LIMIT from top-level
|
|
433
|
-
route = {
|
|
434
|
-
"sql": "SELECT * FROM [[users]]",
|
|
435
|
-
"query_params": [{"age": {"sql": "age = {{age}}"}}]
|
|
436
|
-
}
|
|
437
|
-
db = {"tables": {"users": "test/users.parquet"}, "query_params": [{"limit": {"sql_append": "LIMIT {{limit}}", "default": 100}}]}
|
|
438
|
-
merged = merge_query_params(route, db)
|
|
439
|
-
result = build_sql_query(merged, db, path_params={}, query_params={"age": "30"})
|
|
440
|
-
check_in("integration: WHERE from route", result, "WHERE age = 30")
|
|
441
|
-
check_in("integration: LIMIT from top-level after WHERE", result, "LIMIT 100")
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
# ===========================================================================
|
|
445
|
-
print("\n" + "=" * 50)
|
|
446
|
-
print(f"Results: {passed} passed, {failed} failed")
|
|
447
|
-
if failed == 0:
|
|
448
|
-
print("🎉 All sql_append tests passed!")
|
|
449
|
-
else:
|
|
450
|
-
print(f"💥 {failed} test(s) failed")
|
|
451
|
-
sys.exit(1)
|
|
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
|
|
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
|