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.
Files changed (35) hide show
  1. {api_dock-0.4.8 → api_dock-0.4.9}/PKG-INFO +1 -1
  2. {api_dock-0.4.8 → api_dock-0.4.9}/api_dock/route_mapper.py +1 -1
  3. {api_dock-0.4.8 → api_dock-0.4.9}/api_dock.egg-info/PKG-INFO +1 -1
  4. {api_dock-0.4.8 → api_dock-0.4.9}/api_dock.egg-info/SOURCES.txt +1 -6
  5. {api_dock-0.4.8 → api_dock-0.4.9}/pyproject.toml +1 -1
  6. api_dock-0.4.8/tests/test_config_syntax.py +0 -61
  7. api_dock-0.4.8/tests/test_curl_fixes.py +0 -47
  8. api_dock-0.4.8/tests/test_restrictions.py +0 -108
  9. api_dock-0.4.8/tests/test_root_endpoint.py +0 -44
  10. api_dock-0.4.8/tests/test_sql_append.py +0 -451
  11. {api_dock-0.4.8 → api_dock-0.4.9}/LICENSE.md +0 -0
  12. {api_dock-0.4.8 → api_dock-0.4.9}/README.md +0 -0
  13. {api_dock-0.4.8 → api_dock-0.4.9}/api_dock/__init__.py +0 -0
  14. {api_dock-0.4.8 → api_dock-0.4.9}/api_dock/auth.py +0 -0
  15. {api_dock-0.4.8 → api_dock-0.4.9}/api_dock/cli.py +0 -0
  16. {api_dock-0.4.8 → api_dock-0.4.9}/api_dock/config.py +0 -0
  17. {api_dock-0.4.8 → api_dock-0.4.9}/api_dock/config_discovery.py +0 -0
  18. {api_dock-0.4.8 → api_dock-0.4.9}/api_dock/database_config.py +0 -0
  19. {api_dock-0.4.8 → api_dock-0.4.9}/api_dock/encryption.py +0 -0
  20. {api_dock-0.4.8 → api_dock-0.4.9}/api_dock/fast_api.py +0 -0
  21. {api_dock-0.4.8 → api_dock-0.4.9}/api_dock/flask_api.py +0 -0
  22. {api_dock-0.4.8 → api_dock-0.4.9}/api_dock/sql_builder.py +0 -0
  23. {api_dock-0.4.8 → api_dock-0.4.9}/api_dock/storage_auth.py +0 -0
  24. {api_dock-0.4.8 → api_dock-0.4.9}/api_dock.egg-info/dependency_links.txt +0 -0
  25. {api_dock-0.4.8 → api_dock-0.4.9}/api_dock.egg-info/entry_points.txt +0 -0
  26. {api_dock-0.4.8 → api_dock-0.4.9}/api_dock.egg-info/requires.txt +0 -0
  27. {api_dock-0.4.8 → api_dock-0.4.9}/api_dock.egg-info/top_level.txt +0 -0
  28. {api_dock-0.4.8 → api_dock-0.4.9}/config/config.yaml +0 -0
  29. {api_dock-0.4.8 → api_dock-0.4.9}/config/databases/db_example.yaml +0 -0
  30. {api_dock-0.4.8 → api_dock-0.4.9}/config/databases/test_users.yaml +0 -0
  31. {api_dock-0.4.8 → api_dock-0.4.9}/config/remotes/remote_with_allowed_routes.yaml +0 -0
  32. {api_dock-0.4.8 → api_dock-0.4.9}/config/remotes/remote_with_custom_mapping.yaml +0 -0
  33. {api_dock-0.4.8 → api_dock-0.4.9}/config/remotes/remote_with_restrictions.yaml +0 -0
  34. {api_dock-0.4.8 → api_dock-0.4.9}/config/remotes/remote_with_wildcards.yaml +0 -0
  35. {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.8
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 = True if self.settings.get("add_trailing_slash", True) else False
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.8
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.8"
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