planar 0.9.3__py3-none-any.whl → 0.11.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. planar/ai/agent.py +2 -1
  2. planar/ai/agent_base.py +24 -5
  3. planar/ai/state.py +17 -0
  4. planar/app.py +18 -1
  5. planar/data/connection.py +108 -0
  6. planar/data/dataset.py +11 -104
  7. planar/data/utils.py +89 -0
  8. planar/db/alembic/env.py +25 -1
  9. planar/files/storage/azure_blob.py +1 -1
  10. planar/registry_items.py +2 -0
  11. planar/routers/dataset_router.py +213 -0
  12. planar/routers/info.py +79 -36
  13. planar/routers/models.py +1 -0
  14. planar/routers/workflow.py +2 -0
  15. planar/scaffold_templates/pyproject.toml.j2 +1 -1
  16. planar/security/authorization.py +31 -3
  17. planar/security/default_policies.cedar +25 -0
  18. planar/testing/fixtures.py +34 -1
  19. planar/testing/planar_test_client.py +1 -1
  20. planar/workflows/decorators.py +2 -1
  21. planar/workflows/wrappers.py +1 -0
  22. {planar-0.9.3.dist-info → planar-0.11.0.dist-info}/METADATA +9 -1
  23. {planar-0.9.3.dist-info → planar-0.11.0.dist-info}/RECORD +25 -72
  24. {planar-0.9.3.dist-info → planar-0.11.0.dist-info}/WHEEL +1 -1
  25. planar/ai/test_agent_serialization.py +0 -229
  26. planar/ai/test_agent_tool_step_display.py +0 -78
  27. planar/data/test_dataset.py +0 -354
  28. planar/files/storage/test_azure_blob.py +0 -435
  29. planar/files/storage/test_local_directory.py +0 -162
  30. planar/files/storage/test_s3.py +0 -299
  31. planar/files/test_files.py +0 -282
  32. planar/human/test_human.py +0 -385
  33. planar/logging/test_formatter.py +0 -327
  34. planar/modeling/mixins/test_auditable.py +0 -97
  35. planar/modeling/mixins/test_timestamp.py +0 -134
  36. planar/modeling/mixins/test_uuid_primary_key.py +0 -52
  37. planar/routers/test_agents_router.py +0 -174
  38. planar/routers/test_files_router.py +0 -49
  39. planar/routers/test_object_config_router.py +0 -367
  40. planar/routers/test_routes_security.py +0 -168
  41. planar/routers/test_rule_router.py +0 -470
  42. planar/routers/test_workflow_router.py +0 -539
  43. planar/rules/test_data/account_dormancy_management.json +0 -223
  44. planar/rules/test_data/airline_loyalty_points_calculator.json +0 -262
  45. planar/rules/test_data/applicant_risk_assessment.json +0 -435
  46. planar/rules/test_data/booking_fraud_detection.json +0 -407
  47. planar/rules/test_data/cellular_data_rollover_system.json +0 -258
  48. planar/rules/test_data/clinical_trial_eligibility_screener.json +0 -437
  49. planar/rules/test_data/customer_lifetime_value.json +0 -143
  50. planar/rules/test_data/import_duties_calculator.json +0 -289
  51. planar/rules/test_data/insurance_prior_authorization.json +0 -443
  52. planar/rules/test_data/online_check_in_eligibility_system.json +0 -254
  53. planar/rules/test_data/order_consolidation_system.json +0 -375
  54. planar/rules/test_data/portfolio_risk_monitor.json +0 -471
  55. planar/rules/test_data/supply_chain_risk.json +0 -253
  56. planar/rules/test_data/warehouse_cross_docking.json +0 -237
  57. planar/rules/test_rules.py +0 -1494
  58. planar/security/tests/test_auth_middleware.py +0 -162
  59. planar/security/tests/test_authorization_context.py +0 -78
  60. planar/security/tests/test_cedar_basics.py +0 -41
  61. planar/security/tests/test_cedar_policies.py +0 -158
  62. planar/security/tests/test_jwt_principal_context.py +0 -179
  63. planar/test_app.py +0 -142
  64. planar/test_cli.py +0 -394
  65. planar/test_config.py +0 -515
  66. planar/test_object_config.py +0 -527
  67. planar/test_object_registry.py +0 -14
  68. planar/test_sqlalchemy.py +0 -193
  69. planar/test_utils.py +0 -105
  70. planar/testing/test_memory_storage.py +0 -143
  71. planar/workflows/test_concurrency_detection.py +0 -120
  72. planar/workflows/test_lock_timeout.py +0 -140
  73. planar/workflows/test_serialization.py +0 -1203
  74. planar/workflows/test_suspend_deserialization.py +0 -231
  75. planar/workflows/test_workflow.py +0 -2005
  76. {planar-0.9.3.dist-info → planar-0.11.0.dist-info}/entry_points.txt +0 -0
@@ -1,354 +0,0 @@
1
- """Tests for PlanarDataset."""
2
-
3
- import polars as pl
4
- import pyarrow as pa
5
- import pytest
6
- from ibis import literal
7
-
8
- from planar import PlanarApp
9
- from planar.data import PlanarDataset
10
- from planar.data.config import DataConfig, SQLiteCatalogConfig
11
- from planar.data.exceptions import (
12
- DataError,
13
- DatasetAlreadyExistsError,
14
- DatasetNotFoundError,
15
- )
16
- from planar.files.storage.config import LocalDirectoryConfig
17
- from planar.workflows import step
18
-
19
-
20
- @pytest.fixture
21
- def data_config(tmp_path):
22
- """Create a test data configuration."""
23
- data_dir = tmp_path / "data"
24
- data_dir.mkdir(exist_ok=True)
25
-
26
- catalog_path = data_dir / "test.sqlite"
27
- storage_path = data_dir / "ducklake_files"
28
- storage_path.mkdir(exist_ok=True)
29
-
30
- return DataConfig(
31
- catalog=SQLiteCatalogConfig(type="sqlite", path=str(catalog_path)),
32
- storage=LocalDirectoryConfig(backend="localdir", directory=str(storage_path)),
33
- )
34
-
35
-
36
- @pytest.fixture(name="app")
37
- def app_fixture(data_config):
38
- """Create a PlanarApp with data configuration."""
39
- app = PlanarApp()
40
- # Add data config to the app's config
41
- app.config.data = data_config
42
- return app
43
-
44
-
45
- @pytest.mark.asyncio
46
- async def test_dataset_create(client):
47
- """Test creating a dataset reference."""
48
- dataset = await PlanarDataset.create("test_table")
49
- assert dataset.name == "test_table"
50
-
51
- # Dataset reference exists but table isn't created until first write
52
- assert not await dataset.exists()
53
-
54
- # Write some data to actually create the table
55
- df = pl.DataFrame({"id": [1], "name": ["test"]})
56
- await dataset.write(df, mode="overwrite")
57
-
58
- # Now it should exist
59
- assert await dataset.exists()
60
-
61
- # Cleanup
62
- await dataset.delete()
63
-
64
-
65
- @pytest.mark.asyncio
66
- async def test_dataset_create_if_not_exists(client):
67
- """Test creating a dataset with if_not_exists behavior."""
68
- # Create dataset and write data to make it exist
69
- dataset1 = await PlanarDataset.create("test_table")
70
- df = pl.DataFrame({"id": [1], "name": ["test"]})
71
- await dataset1.write(df, mode="overwrite")
72
-
73
- # Create again with if_not_exists=True (default) - should not raise
74
- dataset2 = await PlanarDataset.create("test_table", if_not_exists=True)
75
- assert dataset2.name == dataset1.name
76
-
77
- # Create again with if_not_exists=False - should raise
78
- with pytest.raises(DatasetAlreadyExistsError):
79
- await PlanarDataset.create("test_table", if_not_exists=False)
80
-
81
- # Cleanup
82
- await dataset1.delete()
83
-
84
-
85
- @pytest.mark.asyncio
86
- async def test_dataset_write_and_read_polars(client):
87
- """Test writing and reading data with Polars."""
88
- dataset = await PlanarDataset.create("test_polars")
89
-
90
- # Create test data
91
- df = pl.DataFrame(
92
- {
93
- "id": [1, 2, 3],
94
- "name": ["Alice", "Bob", "Charlie"],
95
- "amount": [100.5, 200.0, 150.75],
96
- }
97
- )
98
-
99
- # Write data
100
- await dataset.write(df, mode="overwrite")
101
-
102
- # Read data back
103
- result = await dataset.to_polars()
104
-
105
- # Verify
106
- assert result.shape == df.shape
107
- assert set(result.columns) == set(df.columns)
108
- assert result["id"].to_list() == [1, 2, 3]
109
- assert result["name"].to_list() == ["Alice", "Bob", "Charlie"]
110
-
111
- # Cleanup
112
- await dataset.delete()
113
-
114
-
115
- @pytest.mark.asyncio
116
- async def test_dataset_write_and_read_pyarrow(client):
117
- """Test writing and reading data with PyArrow."""
118
- dataset = await PlanarDataset.create("test_pyarrow")
119
-
120
- # Create test data
121
- table = pa.table(
122
- {
123
- "id": [1, 2, 3],
124
- "name": ["Alice", "Bob", "Charlie"],
125
- "amount": [100.5, 200.0, 150.75],
126
- }
127
- )
128
-
129
- # Write data
130
- await dataset.write(table, mode="overwrite")
131
-
132
- # Read data back
133
- result = await dataset.to_pyarrow()
134
-
135
- # Verify
136
- assert result.num_rows == table.num_rows
137
- assert result.column_names == table.column_names
138
-
139
- # Cleanup
140
- await dataset.delete()
141
-
142
-
143
- @pytest.mark.asyncio
144
- async def test_dataset_append_mode(client):
145
- """Test appending data to a dataset."""
146
- dataset = await PlanarDataset.create("test_append")
147
-
148
- # Write initial data
149
- df1 = pl.DataFrame({"id": [1, 2], "value": ["a", "b"]})
150
- await dataset.write(df1, mode="overwrite")
151
-
152
- # Append more data
153
- df2 = pl.DataFrame({"id": [3, 4], "value": ["c", "d"]})
154
- await dataset.write(df2, mode="append")
155
-
156
- result = await dataset.to_polars()
157
-
158
- # Verify
159
- assert len(result) == 4
160
- assert set(result["id"].to_list()) == {1, 2, 3, 4}
161
- assert set(result["value"].to_list()) == {"a", "b", "c", "d"}
162
-
163
- # Cleanup
164
- await dataset.delete()
165
-
166
-
167
- @pytest.mark.asyncio
168
- async def test_dataset_overwrite_replaces_existing(client):
169
- """Overwrite should replace existing rows completely."""
170
- dataset = await PlanarDataset.create("test_overwrite")
171
-
172
- df1 = pl.DataFrame({"id": [1, 2], "value": ["a", "b"]})
173
- await dataset.write(df1, mode="overwrite")
174
- result1 = await dataset.to_polars()
175
- assert result1.shape == (2, 2)
176
-
177
- df2 = pl.DataFrame({"id": [3], "value": ["c"]})
178
- await dataset.write(df2, mode="overwrite")
179
- result2 = await dataset.to_polars()
180
- assert result2.shape == (1, 2)
181
- assert result2["id"].to_list() == [3]
182
- assert result2["value"].to_list() == ["c"]
183
-
184
- await dataset.delete()
185
-
186
-
187
- @pytest.mark.asyncio
188
- async def test_dataset_read_with_filter(client):
189
- """Test reading data with Ibis filtering."""
190
- dataset = await PlanarDataset.create("test_filter")
191
-
192
- # Write test data
193
- df = pl.DataFrame({"id": range(1, 11), "value": range(10, 101, 10)})
194
- await dataset.write(df, mode="overwrite")
195
-
196
- table = await dataset.read()
197
- filtered_table = table.filter(table.value > literal(50))
198
- filtered_df = filtered_table.to_polars()
199
-
200
- assert len(filtered_df) == 5
201
- assert all(v > 50 for v in filtered_df["value"].to_list())
202
-
203
- # Cleanup
204
- await dataset.delete()
205
-
206
-
207
- @pytest.mark.asyncio
208
- async def test_dataset_read_with_columns_and_limit(client):
209
- """Test reading specific columns with limit."""
210
- dataset = await PlanarDataset.create("test_select")
211
-
212
- # Write test data
213
- df = pl.DataFrame(
214
- {
215
- "id": range(1, 11),
216
- "name": [f"user_{i}" for i in range(1, 11)],
217
- "value": range(10, 101, 10),
218
- }
219
- )
220
- await dataset.write(df, mode="overwrite")
221
-
222
- # Read specific columns with limit
223
- table = await dataset.read(columns=["id", "name"], limit=5)
224
- result_df = table.to_polars()
225
-
226
- # Verify
227
- assert len(result_df) == 5
228
- assert set(result_df.columns) == {"id", "name"}
229
- assert "value" not in result_df.columns
230
-
231
- # Cleanup
232
- await dataset.delete()
233
-
234
-
235
- @pytest.mark.asyncio
236
- async def test_dataset_not_found(client):
237
- """Test reading from non-existent dataset."""
238
- dataset = PlanarDataset(name="nonexistent")
239
-
240
- # Check exists returns False
241
- assert not await dataset.exists()
242
-
243
- # Try to read - should raise
244
- with pytest.raises(DatasetNotFoundError):
245
- await dataset.read()
246
-
247
-
248
- @pytest.mark.asyncio
249
- async def test_dataset_delete(client):
250
- """Test deleting a dataset."""
251
- dataset = await PlanarDataset.create("test_delete")
252
-
253
- # Write some data
254
- df = pl.DataFrame({"id": [1, 2, 3]})
255
- await dataset.write(df)
256
-
257
- # Verify it exists
258
- assert await dataset.exists()
259
-
260
- # Delete it
261
- await dataset.delete()
262
-
263
- # Verify it's gone
264
- assert not await dataset.exists()
265
-
266
-
267
- @pytest.mark.asyncio
268
- async def test_dataset_write_list_of_dicts(client):
269
- """Write list-of-dicts input and read back with Polars."""
270
- dataset = await PlanarDataset.create("test_list_of_dicts")
271
-
272
- rows = [{"id": 1, "name": "a"}, {"id": 2, "name": "b"}]
273
- await dataset.write(rows, mode="overwrite")
274
-
275
- result = await dataset.to_polars()
276
- assert set(result.columns) == {"id", "name"}
277
- assert sorted(result["id"].to_list()) == [1, 2]
278
-
279
- await dataset.delete()
280
-
281
-
282
- @pytest.mark.asyncio
283
- async def test_dataset_write_dict_of_lists(client):
284
- """Write dict-of-lists input and read back with Polars."""
285
- dataset = await PlanarDataset.create("test_dict_of_lists")
286
-
287
- data = {"id": [1, 2], "name": ["a", "b"]}
288
- await dataset.write(data, mode="overwrite")
289
-
290
- result = await dataset.to_polars()
291
- assert result.shape == (2, 2)
292
- assert set(result["name"].to_list()) == {"a", "b"}
293
-
294
- await dataset.delete()
295
-
296
-
297
- @pytest.mark.asyncio
298
- async def test_dataset_workflow_serialization(client):
299
- """Test that PlanarDataset can be used as workflow input/output."""
300
-
301
- @step()
302
- async def create_data() -> PlanarDataset:
303
- """Create a dataset with sample data."""
304
- dataset = await PlanarDataset.create("workflow_data")
305
-
306
- df = pl.DataFrame(
307
- {"product": ["A", "B", "C", "D"], "sales": [100, 200, 150, 300]}
308
- )
309
- await dataset.write(df, mode="overwrite")
310
-
311
- return dataset
312
-
313
- @step()
314
- async def analyze_data(dataset: PlanarDataset) -> float:
315
- """Analyze the dataset and return total sales."""
316
- df = await dataset.to_polars()
317
- return float(df["sales"].sum())
318
-
319
- # Test basic workflow functionality without API
320
- dataset = await create_data()
321
- total = await analyze_data(dataset)
322
-
323
- # Verify results
324
- assert total == 750.0 # Sum of [100, 200, 150, 300]
325
-
326
- # Cleanup
327
- await dataset.delete()
328
-
329
-
330
- @pytest.mark.asyncio
331
- async def test_no_data_config_error(client):
332
- """Test error when data config is not set."""
333
- # Remove data config
334
- client.app.config.data = None
335
-
336
- dataset = PlanarDataset(name="test")
337
-
338
- with pytest.raises(DataError, match="Data configuration not found"):
339
- await dataset._get_connection()
340
-
341
-
342
- @pytest.mark.asyncio
343
- async def test_write_with_invalid_input_raises(client):
344
- """Unknown input types to write() should raise a DataError."""
345
-
346
- class Foo:
347
- pass
348
-
349
- dataset = await PlanarDataset.create("test_invalid_input")
350
-
351
- with pytest.raises(DataError):
352
- await dataset.write(Foo(), mode="overwrite") # type: ignore
353
-
354
- await dataset.delete()