cocoindex 0.2.3__cp311-abi3-macosx_10_12_x86_64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,249 @@
1
+ """
2
+ Test suite for optional database functionality in CocoIndex.
3
+
4
+ This module tests that:
5
+ 1. cocoindex.init() works without database settings
6
+ 2. Transform flows work without database
7
+ 3. Database functionality still works when database settings are provided
8
+ 4. Operations requiring database properly complain when no database is configured
9
+ """
10
+
11
+ import os
12
+ from unittest.mock import patch
13
+ import pytest
14
+
15
+ import cocoindex
16
+ from cocoindex import op
17
+ from cocoindex.setting import Settings
18
+
19
+
20
+ class TestOptionalDatabase:
21
+ """Test suite for optional database functionality."""
22
+
23
+ def setup_method(self) -> None:
24
+ """Setup method called before each test."""
25
+ # Stop any existing cocoindex instance
26
+ try:
27
+ cocoindex.stop()
28
+ except:
29
+ pass
30
+
31
+ def teardown_method(self) -> None:
32
+ """Teardown method called after each test."""
33
+ # Stop cocoindex instance after each test
34
+ try:
35
+ cocoindex.stop()
36
+ except:
37
+ pass
38
+
39
+ def test_init_without_database(self) -> None:
40
+ """Test that cocoindex.init() works without database settings."""
41
+ # Remove database environment variables
42
+ with patch.dict(os.environ, {}, clear=False):
43
+ # Remove database env vars if they exist
44
+ for env_var in [
45
+ "COCOINDEX_DATABASE_URL",
46
+ "COCOINDEX_DATABASE_USER",
47
+ "COCOINDEX_DATABASE_PASSWORD",
48
+ ]:
49
+ os.environ.pop(env_var, None)
50
+
51
+ # Test initialization without database
52
+ cocoindex.init()
53
+
54
+ # If we get here without exception, the test passes
55
+ assert True
56
+
57
+ def test_transform_flow_without_database(self) -> None:
58
+ """Test that transform flows work without database."""
59
+ # Remove database environment variables
60
+ with patch.dict(os.environ, {}, clear=False):
61
+ # Remove database env vars if they exist
62
+ for env_var in [
63
+ "COCOINDEX_DATABASE_URL",
64
+ "COCOINDEX_DATABASE_USER",
65
+ "COCOINDEX_DATABASE_PASSWORD",
66
+ ]:
67
+ os.environ.pop(env_var, None)
68
+
69
+ # Initialize without database
70
+ cocoindex.init()
71
+
72
+ # Create a simple custom function for testing
73
+ @op.function()
74
+ def add_prefix(text: str) -> str:
75
+ """Add a prefix to text."""
76
+ return f"processed: {text}"
77
+
78
+ @cocoindex.transform_flow()
79
+ def simple_transform(
80
+ text: cocoindex.DataSlice[str],
81
+ ) -> cocoindex.DataSlice[str]:
82
+ """A simple transform that adds a prefix."""
83
+ return text.transform(add_prefix)
84
+
85
+ # Test the transform flow
86
+ result = simple_transform.eval("hello world")
87
+ expected = "processed: hello world"
88
+
89
+ assert result == expected
90
+
91
+ @pytest.mark.skipif(
92
+ not os.getenv("COCOINDEX_DATABASE_URL"),
93
+ reason="Database URL not configured in environment",
94
+ )
95
+ def test_init_with_database(self) -> None:
96
+ """Test that cocoindex.init() works with database settings when available."""
97
+ # This test only runs if database URL is configured
98
+ settings = Settings.from_env()
99
+ assert settings.database is not None
100
+ assert settings.database.url is not None
101
+
102
+ try:
103
+ cocoindex.init(settings)
104
+ assert True
105
+ except Exception as e:
106
+ assert (
107
+ "Failed to connect to database" in str(e)
108
+ or "connection" in str(e).lower()
109
+ )
110
+
111
+ def test_settings_from_env_without_database(self) -> None:
112
+ """Test that Settings.from_env() correctly handles missing database settings."""
113
+ with patch.dict(os.environ, {}, clear=False):
114
+ # Remove database env vars if they exist
115
+ for env_var in [
116
+ "COCOINDEX_DATABASE_URL",
117
+ "COCOINDEX_DATABASE_USER",
118
+ "COCOINDEX_DATABASE_PASSWORD",
119
+ ]:
120
+ os.environ.pop(env_var, None)
121
+
122
+ settings = Settings.from_env()
123
+ assert settings.database is None
124
+ assert settings.app_namespace == ""
125
+
126
+ def test_settings_from_env_with_database(self) -> None:
127
+ """Test that Settings.from_env() correctly handles database settings when provided."""
128
+ test_url = "postgresql://test:test@localhost:5432/test"
129
+ test_user = "testuser"
130
+ test_password = "testpass"
131
+
132
+ with patch.dict(
133
+ os.environ,
134
+ {
135
+ "COCOINDEX_DATABASE_URL": test_url,
136
+ "COCOINDEX_DATABASE_USER": test_user,
137
+ "COCOINDEX_DATABASE_PASSWORD": test_password,
138
+ },
139
+ ):
140
+ settings = Settings.from_env()
141
+ assert settings.database is not None
142
+ assert settings.database.url == test_url
143
+ assert settings.database.user == test_user
144
+ assert settings.database.password == test_password
145
+
146
+ def test_settings_from_env_with_partial_database_config(self) -> None:
147
+ """Test Settings.from_env() with only database URL (no user/password)."""
148
+ test_url = "postgresql://localhost:5432/test"
149
+
150
+ with patch.dict(
151
+ os.environ,
152
+ {
153
+ "COCOINDEX_DATABASE_URL": test_url,
154
+ },
155
+ clear=False,
156
+ ):
157
+ # Remove user/password env vars if they exist
158
+ os.environ.pop("COCOINDEX_DATABASE_USER", None)
159
+ os.environ.pop("COCOINDEX_DATABASE_PASSWORD", None)
160
+
161
+ settings = Settings.from_env()
162
+ assert settings.database is not None
163
+ assert settings.database.url == test_url
164
+ assert settings.database.user is None
165
+ assert settings.database.password is None
166
+
167
+ def test_multiple_init_calls(self) -> None:
168
+ """Test that multiple init calls work correctly."""
169
+ with patch.dict(os.environ, {}, clear=False):
170
+ # Remove database env vars if they exist
171
+ for env_var in [
172
+ "COCOINDEX_DATABASE_URL",
173
+ "COCOINDEX_DATABASE_USER",
174
+ "COCOINDEX_DATABASE_PASSWORD",
175
+ ]:
176
+ os.environ.pop(env_var, None)
177
+
178
+ # First init
179
+ cocoindex.init()
180
+
181
+ # Stop and init again
182
+ cocoindex.stop()
183
+ cocoindex.init()
184
+
185
+ # Should work without issues
186
+ assert True
187
+
188
+ def test_app_namespace_setting(self) -> None:
189
+ """Test that app_namespace setting works correctly."""
190
+ test_namespace = "test_app"
191
+
192
+ with patch.dict(
193
+ os.environ,
194
+ {
195
+ "COCOINDEX_APP_NAMESPACE": test_namespace,
196
+ },
197
+ clear=False,
198
+ ):
199
+ # Remove database env vars if they exist
200
+ for env_var in [
201
+ "COCOINDEX_DATABASE_URL",
202
+ "COCOINDEX_DATABASE_USER",
203
+ "COCOINDEX_DATABASE_PASSWORD",
204
+ ]:
205
+ os.environ.pop(env_var, None)
206
+
207
+ settings = Settings.from_env()
208
+ assert settings.app_namespace == test_namespace
209
+ assert settings.database is None
210
+
211
+ # Init should work with app namespace but no database
212
+ cocoindex.init(settings)
213
+ assert True
214
+
215
+
216
+ class TestDatabaseRequiredOperations:
217
+ """Test suite for operations that require database."""
218
+
219
+ def setup_method(self) -> None:
220
+ """Setup method called before each test."""
221
+ # Stop any existing cocoindex instance
222
+ try:
223
+ cocoindex.stop()
224
+ except:
225
+ pass
226
+
227
+ def teardown_method(self) -> None:
228
+ """Teardown method called after each test."""
229
+ # Stop cocoindex instance after each test
230
+ try:
231
+ cocoindex.stop()
232
+ except:
233
+ pass
234
+
235
+ def test_database_required_error_message(self) -> None:
236
+ """Test that operations requiring database show proper error messages."""
237
+ with patch.dict(os.environ, {}, clear=False):
238
+ # Remove database env vars if they exist
239
+ for env_var in [
240
+ "COCOINDEX_DATABASE_URL",
241
+ "COCOINDEX_DATABASE_USER",
242
+ "COCOINDEX_DATABASE_PASSWORD",
243
+ ]:
244
+ os.environ.pop(env_var, None)
245
+
246
+ # Initialize without database
247
+ cocoindex.init()
248
+
249
+ assert True
@@ -0,0 +1,207 @@
1
+ import typing
2
+ from dataclasses import dataclass
3
+ from typing import Any
4
+
5
+ import pytest
6
+
7
+ import cocoindex
8
+
9
+
10
+ @dataclass
11
+ class Child:
12
+ value: int
13
+
14
+
15
+ @dataclass
16
+ class Parent:
17
+ children: list[Child]
18
+
19
+
20
+ # Fixture to initialize CocoIndex library
21
+ @pytest.fixture(scope="session", autouse=True)
22
+ def init_cocoindex() -> typing.Generator[None, None, None]:
23
+ cocoindex.init()
24
+ yield
25
+
26
+
27
+ @cocoindex.op.function()
28
+ def add_suffix(text: str) -> str:
29
+ """Append ' world' to the input text."""
30
+ return f"{text} world"
31
+
32
+
33
+ @cocoindex.transform_flow()
34
+ def simple_transform(text: cocoindex.DataSlice[str]) -> cocoindex.DataSlice[str]:
35
+ """Transform flow that applies add_suffix to input text."""
36
+ return text.transform(add_suffix)
37
+
38
+
39
+ @cocoindex.op.function()
40
+ def extract_value(value: int) -> int:
41
+ """Extracts the value."""
42
+ return value
43
+
44
+
45
+ @cocoindex.transform_flow()
46
+ def for_each_transform(
47
+ data: cocoindex.DataSlice[Parent],
48
+ ) -> cocoindex.DataSlice[Any]:
49
+ """Transform flow that processes child rows to extract values."""
50
+ with data["children"].row() as child:
51
+ child["new_field"] = child["value"].transform(extract_value)
52
+ return data
53
+
54
+
55
+ def test_simple_transform_flow() -> None:
56
+ """Test the simple transform flow."""
57
+ input_text = "hello"
58
+ result = simple_transform.eval(input_text)
59
+ assert result == "hello world", f"Expected 'hello world', got {result}"
60
+
61
+ result = simple_transform.eval("")
62
+ assert result == " world", f"Expected ' world', got {result}"
63
+
64
+
65
+ @pytest.mark.asyncio
66
+ async def test_simple_transform_flow_async() -> None:
67
+ """Test the simple transform flow asynchronously."""
68
+ input_text = "async"
69
+ result = await simple_transform.eval_async(input_text)
70
+ assert result == "async world", f"Expected 'async world', got {result}"
71
+
72
+
73
+ def test_for_each_transform_flow() -> None:
74
+ """Test the complex transform flow with child rows."""
75
+ input_data = Parent(children=[Child(1), Child(2), Child(3)])
76
+ result = for_each_transform.eval(input_data)
77
+ expected = {
78
+ "children": [
79
+ {"value": 1, "new_field": 1},
80
+ {"value": 2, "new_field": 2},
81
+ {"value": 3, "new_field": 3},
82
+ ]
83
+ }
84
+ assert result == expected, f"Expected {expected}, got {result}"
85
+
86
+ input_data = Parent(children=[])
87
+ result = for_each_transform.eval(input_data)
88
+ assert result == {"children": []}, f"Expected {{'children': []}}, got {result}"
89
+
90
+
91
+ @pytest.mark.asyncio
92
+ async def test_for_each_transform_flow_async() -> None:
93
+ """Test the complex transform flow asynchronously."""
94
+ input_data = Parent(children=[Child(4), Child(5)])
95
+ result = await for_each_transform.eval_async(input_data)
96
+ expected = {
97
+ "children": [
98
+ {"value": 4, "new_field": 4},
99
+ {"value": 5, "new_field": 5},
100
+ ]
101
+ }
102
+
103
+ assert result == expected, f"Expected {expected}, got {result}"
104
+
105
+
106
+ def test_none_arg_yield_none_result() -> None:
107
+ """Test that None arguments yield None results."""
108
+
109
+ @cocoindex.op.function()
110
+ def custom_fn(
111
+ required_arg: int,
112
+ optional_arg: int | None,
113
+ required_kwarg: int,
114
+ optional_kwarg: int | None,
115
+ ) -> int:
116
+ return (
117
+ required_arg + (optional_arg or 0) + required_kwarg + (optional_kwarg or 0)
118
+ )
119
+
120
+ @cocoindex.transform_flow()
121
+ def transform_flow(
122
+ required_arg: cocoindex.DataSlice[int | None],
123
+ optional_arg: cocoindex.DataSlice[int | None],
124
+ required_kwarg: cocoindex.DataSlice[int | None],
125
+ optional_kwarg: cocoindex.DataSlice[int | None],
126
+ ) -> cocoindex.DataSlice[int | None]:
127
+ return required_arg.transform(
128
+ custom_fn,
129
+ optional_arg,
130
+ required_kwarg=required_kwarg,
131
+ optional_kwarg=optional_kwarg,
132
+ )
133
+
134
+ result = transform_flow.eval(1, 2, 4, 8)
135
+ assert result == 15, f"Expected 15, got {result}"
136
+
137
+ result = transform_flow.eval(1, None, 4, None)
138
+ assert result == 5, f"Expected 5, got {result}"
139
+
140
+ result = transform_flow.eval(None, 2, 4, 8)
141
+ assert result is None, f"Expected None, got {result}"
142
+
143
+ result = transform_flow.eval(1, 2, None, None)
144
+ assert result is None, f"Expected None, got {result}"
145
+
146
+
147
+ # Test GPU function behavior.
148
+ # They're not really executed on GPU, but we want to make sure they're scheduled on subprocesses correctly.
149
+
150
+
151
+ @cocoindex.op.function(gpu=True)
152
+ def gpu_append_world(text: str) -> str:
153
+ """Append ' world' to the input text."""
154
+ return f"{text} world"
155
+
156
+
157
+ class GpuAppendSuffix(cocoindex.op.FunctionSpec):
158
+ suffix: str
159
+
160
+
161
+ @cocoindex.op.executor_class(gpu=True)
162
+ class GpuAppendSuffixExecutor:
163
+ spec: GpuAppendSuffix
164
+
165
+ def __call__(self, text: str) -> str:
166
+ return f"{text}{self.spec.suffix}"
167
+
168
+
169
+ class GpuAppendSuffixWithAnalyzePrepare(cocoindex.op.FunctionSpec):
170
+ suffix: str
171
+
172
+
173
+ @cocoindex.op.executor_class(gpu=True)
174
+ class GpuAppendSuffixWithAnalyzePrepareExecutor:
175
+ spec: GpuAppendSuffixWithAnalyzePrepare
176
+ suffix: str
177
+
178
+ def analyze(self) -> Any:
179
+ return str
180
+
181
+ def prepare(self) -> None:
182
+ self.suffix = self.spec.suffix
183
+
184
+ def __call__(self, text: str) -> str:
185
+ return f"{text}{self.suffix}"
186
+
187
+
188
+ def test_gpu_function() -> None:
189
+ @cocoindex.transform_flow()
190
+ def transform_flow(text: cocoindex.DataSlice[str]) -> cocoindex.DataSlice[str]:
191
+ return text.transform(gpu_append_world).transform(GpuAppendSuffix(suffix="!"))
192
+
193
+ result = transform_flow.eval("Hello")
194
+ expected = "Hello world!"
195
+ assert result == expected, f"Expected {expected}, got {result}"
196
+
197
+ @cocoindex.transform_flow()
198
+ def transform_flow_with_analyze_prepare(
199
+ text: cocoindex.DataSlice[str],
200
+ ) -> cocoindex.DataSlice[str]:
201
+ return text.transform(gpu_append_world).transform(
202
+ GpuAppendSuffixWithAnalyzePrepare(suffix="!!")
203
+ )
204
+
205
+ result = transform_flow_with_analyze_prepare.eval("Hello")
206
+ expected = "Hello world!!"
207
+ assert result == expected, f"Expected {expected}, got {result}"