planar 0.10.0__py3-none-any.whl → 0.12.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.
- planar/app.py +26 -6
- planar/cli.py +26 -0
- planar/data/__init__.py +1 -0
- planar/data/config.py +12 -1
- planar/data/connection.py +89 -4
- planar/data/dataset.py +13 -7
- planar/data/utils.py +145 -25
- planar/db/alembic/env.py +68 -57
- planar/db/alembic.ini +1 -1
- planar/files/storage/config.py +7 -1
- planar/routers/dataset_router.py +5 -1
- planar/routers/info.py +79 -36
- planar/scaffold_templates/pyproject.toml.j2 +1 -1
- planar/testing/fixtures.py +7 -4
- planar/testing/planar_test_client.py +8 -0
- planar/version.py +27 -0
- planar-0.12.0.dist-info/METADATA +202 -0
- {planar-0.10.0.dist-info → planar-0.12.0.dist-info}/RECORD +20 -71
- planar/ai/test_agent_serialization.py +0 -229
- planar/ai/test_agent_tool_step_display.py +0 -78
- planar/data/test_dataset.py +0 -358
- planar/files/storage/test_azure_blob.py +0 -435
- planar/files/storage/test_local_directory.py +0 -162
- planar/files/storage/test_s3.py +0 -299
- planar/files/test_files.py +0 -282
- planar/human/test_human.py +0 -385
- planar/logging/test_formatter.py +0 -327
- planar/modeling/mixins/test_auditable.py +0 -97
- planar/modeling/mixins/test_timestamp.py +0 -134
- planar/modeling/mixins/test_uuid_primary_key.py +0 -52
- planar/routers/test_agents_router.py +0 -174
- planar/routers/test_dataset_router.py +0 -429
- planar/routers/test_files_router.py +0 -49
- planar/routers/test_object_config_router.py +0 -367
- planar/routers/test_routes_security.py +0 -168
- planar/routers/test_rule_router.py +0 -470
- planar/routers/test_workflow_router.py +0 -564
- planar/rules/test_data/account_dormancy_management.json +0 -223
- planar/rules/test_data/airline_loyalty_points_calculator.json +0 -262
- planar/rules/test_data/applicant_risk_assessment.json +0 -435
- planar/rules/test_data/booking_fraud_detection.json +0 -407
- planar/rules/test_data/cellular_data_rollover_system.json +0 -258
- planar/rules/test_data/clinical_trial_eligibility_screener.json +0 -437
- planar/rules/test_data/customer_lifetime_value.json +0 -143
- planar/rules/test_data/import_duties_calculator.json +0 -289
- planar/rules/test_data/insurance_prior_authorization.json +0 -443
- planar/rules/test_data/online_check_in_eligibility_system.json +0 -254
- planar/rules/test_data/order_consolidation_system.json +0 -375
- planar/rules/test_data/portfolio_risk_monitor.json +0 -471
- planar/rules/test_data/supply_chain_risk.json +0 -253
- planar/rules/test_data/warehouse_cross_docking.json +0 -237
- planar/rules/test_rules.py +0 -1494
- planar/security/tests/test_auth_middleware.py +0 -162
- planar/security/tests/test_authorization_context.py +0 -78
- planar/security/tests/test_cedar_basics.py +0 -41
- planar/security/tests/test_cedar_policies.py +0 -158
- planar/security/tests/test_jwt_principal_context.py +0 -179
- planar/test_app.py +0 -142
- planar/test_cli.py +0 -394
- planar/test_config.py +0 -515
- planar/test_object_config.py +0 -527
- planar/test_object_registry.py +0 -14
- planar/test_sqlalchemy.py +0 -193
- planar/test_utils.py +0 -105
- planar/testing/test_memory_storage.py +0 -143
- planar/workflows/test_concurrency_detection.py +0 -120
- planar/workflows/test_lock_timeout.py +0 -140
- planar/workflows/test_serialization.py +0 -1203
- planar/workflows/test_suspend_deserialization.py +0 -231
- planar/workflows/test_workflow.py +0 -2005
- planar-0.10.0.dist-info/METADATA +0 -323
- {planar-0.10.0.dist-info → planar-0.12.0.dist-info}/WHEEL +0 -0
- {planar-0.10.0.dist-info → planar-0.12.0.dist-info}/entry_points.txt +0 -0
@@ -1,174 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
Tests for agent router endpoints.
|
3
|
-
|
4
|
-
This module tests the agent router endpoints to ensure they work correctly
|
5
|
-
with the new serialization changes.
|
6
|
-
"""
|
7
|
-
|
8
|
-
import pytest
|
9
|
-
from sqlmodel.ext.asyncio.session import AsyncSession
|
10
|
-
|
11
|
-
from planar.ai.agent import Agent
|
12
|
-
from planar.app import PlanarApp
|
13
|
-
from planar.config import sqlite_config
|
14
|
-
from planar.testing.planar_test_client import PlanarTestClient
|
15
|
-
|
16
|
-
|
17
|
-
@pytest.fixture(name="app")
|
18
|
-
def app_fixture(tmp_db_path: str):
|
19
|
-
"""Create a test app with agents."""
|
20
|
-
app = PlanarApp(
|
21
|
-
config=sqlite_config(tmp_db_path),
|
22
|
-
title="Test app for agent router",
|
23
|
-
description="Testing agent endpoints",
|
24
|
-
)
|
25
|
-
|
26
|
-
# Register a simple agent
|
27
|
-
simple_agent = Agent(
|
28
|
-
name="simple_test_agent",
|
29
|
-
system_prompt="Simple system prompt",
|
30
|
-
user_prompt="Simple user prompt: {input}",
|
31
|
-
model="openai:gpt-4o",
|
32
|
-
max_turns=2,
|
33
|
-
)
|
34
|
-
app.register_agent(simple_agent)
|
35
|
-
|
36
|
-
# Register an agent with tools
|
37
|
-
async def test_tool(param: str) -> str:
|
38
|
-
"""A test tool."""
|
39
|
-
return f"Processed: {param}"
|
40
|
-
|
41
|
-
agent_with_tools = Agent(
|
42
|
-
name="agent_with_tools",
|
43
|
-
system_prompt="System with tools",
|
44
|
-
user_prompt="User: {input}",
|
45
|
-
model="anthropic:claude-3-5-sonnet-latest",
|
46
|
-
max_turns=5,
|
47
|
-
tools=[test_tool],
|
48
|
-
)
|
49
|
-
app.register_agent(agent_with_tools)
|
50
|
-
|
51
|
-
return app
|
52
|
-
|
53
|
-
|
54
|
-
async def test_get_agents_endpoint(
|
55
|
-
client: PlanarTestClient, app: PlanarApp, session: AsyncSession
|
56
|
-
):
|
57
|
-
"""Test the GET /agents endpoint returns agents with configs field."""
|
58
|
-
response = await client.get("/planar/v1/agents/")
|
59
|
-
assert response.status_code == 200
|
60
|
-
|
61
|
-
agents = response.json()
|
62
|
-
assert len(agents) == 2
|
63
|
-
|
64
|
-
# Check first agent
|
65
|
-
simple_agent = next(a for a in agents if a["name"] == "simple_test_agent")
|
66
|
-
assert simple_agent["name"] == "simple_test_agent"
|
67
|
-
assert "configs" in simple_agent
|
68
|
-
assert isinstance(simple_agent["configs"], list)
|
69
|
-
assert len(simple_agent["configs"]) == 1 # Default config always present
|
70
|
-
|
71
|
-
# Verify the default config is present and correct
|
72
|
-
default_config = simple_agent["configs"][-1]
|
73
|
-
assert default_config["version"] == 0
|
74
|
-
assert default_config["data"]["system_prompt"] == "Simple system prompt"
|
75
|
-
assert default_config["data"]["user_prompt"] == "Simple user prompt: {input}"
|
76
|
-
assert default_config["data"]["model"] == "openai:gpt-4o"
|
77
|
-
assert default_config["data"]["max_turns"] == 2
|
78
|
-
|
79
|
-
# Verify removed fields are not present
|
80
|
-
assert "system_prompt" not in simple_agent
|
81
|
-
assert "user_prompt" not in simple_agent
|
82
|
-
assert "model" not in simple_agent
|
83
|
-
assert "max_turns" not in simple_agent
|
84
|
-
assert "overwrites" not in simple_agent
|
85
|
-
|
86
|
-
# Check agent with tools
|
87
|
-
tools_agent = next(a for a in agents if a["name"] == "agent_with_tools")
|
88
|
-
assert len(tools_agent["tool_definitions"]) == 1
|
89
|
-
assert tools_agent["tool_definitions"][0]["name"] == "test_tool"
|
90
|
-
|
91
|
-
|
92
|
-
async def test_update_agent_endpoint(
|
93
|
-
client: PlanarTestClient, app: PlanarApp, session: AsyncSession
|
94
|
-
):
|
95
|
-
"""Test the PATCH /agents/{agent_name} endpoint creates configs."""
|
96
|
-
# Get agents first
|
97
|
-
response = await client.get("/planar/v1/agents/")
|
98
|
-
assert response.status_code == 200
|
99
|
-
agents = response.json()
|
100
|
-
assert len(agents) == 2
|
101
|
-
|
102
|
-
# Update the agent
|
103
|
-
update_data = {
|
104
|
-
"system_prompt": "Updated system prompt",
|
105
|
-
"user_prompt": "Updated user prompt: {input}",
|
106
|
-
}
|
107
|
-
response = await client.patch(
|
108
|
-
"/planar/v1/agents/simple_test_agent", json=update_data
|
109
|
-
)
|
110
|
-
assert response.status_code == 200
|
111
|
-
|
112
|
-
updated_agent = response.json()
|
113
|
-
assert "configs" in updated_agent
|
114
|
-
assert len(updated_agent["configs"]) == 2
|
115
|
-
|
116
|
-
# Check the config data
|
117
|
-
config = updated_agent["configs"][0]
|
118
|
-
assert config["data"]["system_prompt"] == "Updated system prompt"
|
119
|
-
assert config["data"]["user_prompt"] == "Updated user prompt: {input}"
|
120
|
-
assert config["version"] == 1
|
121
|
-
assert config["object_type"] == "agent"
|
122
|
-
assert config["object_name"] == "simple_test_agent"
|
123
|
-
|
124
|
-
|
125
|
-
async def test_agent_with_multiple_configs(
|
126
|
-
client: PlanarTestClient, app: PlanarApp, session: AsyncSession
|
127
|
-
):
|
128
|
-
"""Test that agents return all configs when multiple exist."""
|
129
|
-
# Get the agent ID first
|
130
|
-
response = await client.get("/planar/v1/agents/")
|
131
|
-
agents = response.json()
|
132
|
-
simple_agent = next(a for a in agents if a["name"] == "simple_test_agent")
|
133
|
-
|
134
|
-
# Create first config via PATCH endpoint
|
135
|
-
config1_data = {
|
136
|
-
"system_prompt": "Config 1 system",
|
137
|
-
"user_prompt": "Config 1 user: {input}",
|
138
|
-
"model": "openai:gpt-4o",
|
139
|
-
"max_turns": 2,
|
140
|
-
"model_parameters": {"temperature": 0.7},
|
141
|
-
}
|
142
|
-
response = await client.patch(
|
143
|
-
f"/planar/v1/agents/{simple_agent['name']}", json=config1_data
|
144
|
-
)
|
145
|
-
assert response.status_code == 200
|
146
|
-
|
147
|
-
# Create second config via PATCH endpoint
|
148
|
-
config2_data = {
|
149
|
-
"system_prompt": "Config 2 system",
|
150
|
-
"user_prompt": "Config 2 user: {input}",
|
151
|
-
"model": "anthropic:claude-3-opus",
|
152
|
-
"max_turns": 4,
|
153
|
-
"model_parameters": {"temperature": 0.9},
|
154
|
-
}
|
155
|
-
response = await client.patch(
|
156
|
-
f"/planar/v1/agents/{simple_agent['name']}", json=config2_data
|
157
|
-
)
|
158
|
-
assert response.status_code == 200
|
159
|
-
|
160
|
-
# Get agents
|
161
|
-
response = await client.get("/planar/v1/agents/")
|
162
|
-
agents = response.json()
|
163
|
-
simple_agent = next(a for a in agents if a["name"] == "simple_test_agent")
|
164
|
-
|
165
|
-
# Verify all configs are returned (including default config)
|
166
|
-
assert len(simple_agent["configs"]) == 3
|
167
|
-
assert simple_agent["configs"][0]["version"] == 2 # Latest first
|
168
|
-
assert simple_agent["configs"][1]["version"] == 1
|
169
|
-
assert simple_agent["configs"][2]["version"] == 0 # Default config
|
170
|
-
|
171
|
-
# Verify config data
|
172
|
-
assert simple_agent["configs"][0]["data"]["system_prompt"] == "Config 2 system"
|
173
|
-
assert simple_agent["configs"][1]["data"]["system_prompt"] == "Config 1 system"
|
174
|
-
assert simple_agent["configs"][2]["data"]["system_prompt"] == "Simple system prompt"
|
@@ -1,429 +0,0 @@
|
|
1
|
-
import math
|
2
|
-
|
3
|
-
import polars as pl
|
4
|
-
import pyarrow as pa
|
5
|
-
import pytest
|
6
|
-
|
7
|
-
from planar.data.dataset import PlanarDataset
|
8
|
-
from planar.testing.planar_test_client import PlanarTestClient
|
9
|
-
|
10
|
-
|
11
|
-
@pytest.fixture(name="app")
|
12
|
-
def app_fixture(app_with_data):
|
13
|
-
"""Use the shared app_with_data fixture as 'app' for this test module."""
|
14
|
-
return app_with_data
|
15
|
-
|
16
|
-
|
17
|
-
async def test_stream_arrow_chunks(
|
18
|
-
client: PlanarTestClient,
|
19
|
-
):
|
20
|
-
dataset_name = "test_streaming"
|
21
|
-
dataset_size = 10_000
|
22
|
-
batch_size = 1000
|
23
|
-
|
24
|
-
dataset = await PlanarDataset.create(dataset_name)
|
25
|
-
|
26
|
-
df = pl.DataFrame({"id": range(dataset_size)}).with_columns(
|
27
|
-
pl.format("value_{}", pl.col("id")).alias("value")
|
28
|
-
)
|
29
|
-
|
30
|
-
await dataset.write(df)
|
31
|
-
|
32
|
-
response = await client.get(
|
33
|
-
f"/planar/v1/datasets/content/{dataset_name}/arrow-stream",
|
34
|
-
params={"batch_size": batch_size, "limit": dataset_size},
|
35
|
-
)
|
36
|
-
|
37
|
-
assert response.status_code == 200
|
38
|
-
assert response.headers["content-type"] == "application/vnd.apache.arrow.stream"
|
39
|
-
assert "test_streaming.arrow" in response.headers.get("content-disposition", "")
|
40
|
-
assert response.headers.get("x-batch-size") == str(batch_size)
|
41
|
-
|
42
|
-
content = await response.aread()
|
43
|
-
buffer = pa.py_buffer(content)
|
44
|
-
reader = pa.ipc.open_stream(buffer)
|
45
|
-
|
46
|
-
batch_info = []
|
47
|
-
total_rows_received = 0
|
48
|
-
all_ids = []
|
49
|
-
|
50
|
-
try:
|
51
|
-
while True:
|
52
|
-
arrow_batch = reader.read_next_batch()
|
53
|
-
batch_info.append(
|
54
|
-
{
|
55
|
-
"rows": arrow_batch.num_rows,
|
56
|
-
"columns": arrow_batch.num_columns,
|
57
|
-
}
|
58
|
-
)
|
59
|
-
total_rows_received += arrow_batch.num_rows
|
60
|
-
|
61
|
-
id_column = arrow_batch.column("id")
|
62
|
-
batch_ids = id_column.to_pylist()
|
63
|
-
all_ids.extend(batch_ids)
|
64
|
-
except StopIteration:
|
65
|
-
pass
|
66
|
-
|
67
|
-
expected_batches = math.ceil(dataset_size / batch_size)
|
68
|
-
|
69
|
-
assert len(batch_info) == expected_batches
|
70
|
-
assert total_rows_received == dataset_size
|
71
|
-
|
72
|
-
# Verify data integrity - check that we received all expected IDs
|
73
|
-
assert len(all_ids) == dataset_size
|
74
|
-
assert set(all_ids) == set(range(dataset_size))
|
75
|
-
assert sum(all_ids) == sum(range(dataset_size))
|
76
|
-
|
77
|
-
|
78
|
-
async def test_stream_arrow_with_limit(
|
79
|
-
client: PlanarTestClient,
|
80
|
-
):
|
81
|
-
"""Test that the limit parameter properly restricts the number of rows streamed."""
|
82
|
-
dataset_name = "test_streaming_limit"
|
83
|
-
dataset_size = 1000
|
84
|
-
batch_size = 100
|
85
|
-
row_limit = 250 # Should get 3 batches (100 + 100 + 50)
|
86
|
-
|
87
|
-
dataset = await PlanarDataset.create(dataset_name)
|
88
|
-
|
89
|
-
# Create test data
|
90
|
-
df = pl.DataFrame({"id": range(dataset_size)}).with_columns(
|
91
|
-
pl.format("value_{}", pl.col("id")).alias("value")
|
92
|
-
)
|
93
|
-
|
94
|
-
await dataset.write(df)
|
95
|
-
|
96
|
-
response = await client.get(
|
97
|
-
f"/planar/v1/datasets/content/{dataset_name}/arrow-stream",
|
98
|
-
params={"batch_size": batch_size, "limit": row_limit},
|
99
|
-
)
|
100
|
-
|
101
|
-
assert response.status_code == 200
|
102
|
-
assert response.headers["x-row-limit"] == str(row_limit)
|
103
|
-
|
104
|
-
content = await response.aread()
|
105
|
-
buffer = pa.py_buffer(content)
|
106
|
-
reader = pa.ipc.open_stream(buffer)
|
107
|
-
|
108
|
-
total_rows_received = 0
|
109
|
-
batch_count = 0
|
110
|
-
|
111
|
-
try:
|
112
|
-
while True:
|
113
|
-
arrow_batch = reader.read_next_batch()
|
114
|
-
total_rows_received += arrow_batch.num_rows
|
115
|
-
batch_count += 1
|
116
|
-
except StopIteration:
|
117
|
-
pass
|
118
|
-
|
119
|
-
# Should receive exactly the limited number of rows
|
120
|
-
assert total_rows_received == row_limit
|
121
|
-
# Should receive expected number of batches (3: 100, 100, 50)
|
122
|
-
expected_batches = math.ceil(row_limit / batch_size)
|
123
|
-
assert batch_count == expected_batches
|
124
|
-
|
125
|
-
|
126
|
-
async def test_stream_arrow_empty_dataset(
|
127
|
-
client: PlanarTestClient,
|
128
|
-
):
|
129
|
-
"""Test streaming behavior with an empty dataset."""
|
130
|
-
dataset_name = "test_empty_stream"
|
131
|
-
batch_size = 100
|
132
|
-
|
133
|
-
dataset = await PlanarDataset.create(dataset_name)
|
134
|
-
|
135
|
-
# Create empty dataset
|
136
|
-
df = pl.DataFrame(
|
137
|
-
{"id": [], "value": []}, schema={"id": pl.Int64, "value": pl.Utf8}
|
138
|
-
)
|
139
|
-
await dataset.write(df)
|
140
|
-
|
141
|
-
response = await client.get(
|
142
|
-
f"/planar/v1/datasets/content/{dataset_name}/arrow-stream",
|
143
|
-
params={"batch_size": batch_size},
|
144
|
-
)
|
145
|
-
|
146
|
-
assert response.status_code == 200
|
147
|
-
|
148
|
-
content = await response.aread()
|
149
|
-
buffer = pa.py_buffer(content)
|
150
|
-
reader = pa.ipc.open_stream(buffer)
|
151
|
-
|
152
|
-
# Should be able to read the schema and get one empty batch
|
153
|
-
total_rows = 0
|
154
|
-
batch_count = 0
|
155
|
-
|
156
|
-
try:
|
157
|
-
while True:
|
158
|
-
arrow_batch = reader.read_next_batch()
|
159
|
-
total_rows += arrow_batch.num_rows
|
160
|
-
batch_count += 1
|
161
|
-
except StopIteration:
|
162
|
-
pass
|
163
|
-
|
164
|
-
# Should have exactly 1 empty batch (our fallback for empty datasets)
|
165
|
-
assert batch_count == 1
|
166
|
-
assert total_rows == 0
|
167
|
-
|
168
|
-
|
169
|
-
async def test_stream_arrow_single_batch(
|
170
|
-
client: PlanarTestClient,
|
171
|
-
):
|
172
|
-
"""Test streaming when dataset size is smaller than batch size."""
|
173
|
-
dataset_name = "test_single_batch"
|
174
|
-
dataset_size = 50
|
175
|
-
batch_size = 100
|
176
|
-
|
177
|
-
dataset = await PlanarDataset.create(dataset_name)
|
178
|
-
|
179
|
-
df = pl.DataFrame({"id": range(dataset_size)}).with_columns(
|
180
|
-
pl.format("value_{}", pl.col("id")).alias("value")
|
181
|
-
)
|
182
|
-
|
183
|
-
await dataset.write(df)
|
184
|
-
|
185
|
-
response = await client.get(
|
186
|
-
f"/planar/v1/datasets/content/{dataset_name}/arrow-stream",
|
187
|
-
params={"batch_size": batch_size},
|
188
|
-
)
|
189
|
-
|
190
|
-
assert response.status_code == 200
|
191
|
-
|
192
|
-
content = await response.aread()
|
193
|
-
buffer = pa.py_buffer(content)
|
194
|
-
reader = pa.ipc.open_stream(buffer)
|
195
|
-
|
196
|
-
total_rows = 0
|
197
|
-
batch_count = 0
|
198
|
-
|
199
|
-
try:
|
200
|
-
while True:
|
201
|
-
arrow_batch = reader.read_next_batch()
|
202
|
-
total_rows += arrow_batch.num_rows
|
203
|
-
batch_count += 1
|
204
|
-
except StopIteration:
|
205
|
-
pass
|
206
|
-
|
207
|
-
assert batch_count == 1
|
208
|
-
assert total_rows == dataset_size
|
209
|
-
|
210
|
-
|
211
|
-
async def test_get_schemas_endpoint(
|
212
|
-
client: PlanarTestClient,
|
213
|
-
):
|
214
|
-
"""Test the GET /schemas endpoint."""
|
215
|
-
response = await client.get("/planar/v1/datasets/schemas")
|
216
|
-
|
217
|
-
assert response.status_code == 200
|
218
|
-
schemas = response.json()
|
219
|
-
assert isinstance(schemas, list)
|
220
|
-
assert "main" in schemas # Default schema should exist
|
221
|
-
|
222
|
-
|
223
|
-
async def test_list_datasets_metadata_endpoint(
|
224
|
-
client: PlanarTestClient,
|
225
|
-
):
|
226
|
-
"""Test the GET /metadata endpoint (list all datasets)."""
|
227
|
-
# Create a test dataset first
|
228
|
-
dataset_name = "test_list_datasets"
|
229
|
-
dataset = await PlanarDataset.create(dataset_name)
|
230
|
-
|
231
|
-
df = pl.DataFrame({"id": [1, 2, 3], "name": ["a", "b", "c"]})
|
232
|
-
await dataset.write(df)
|
233
|
-
|
234
|
-
response = await client.get("/planar/v1/datasets/metadata")
|
235
|
-
|
236
|
-
assert response.status_code == 200
|
237
|
-
datasets = response.json()
|
238
|
-
assert isinstance(datasets, list)
|
239
|
-
|
240
|
-
# Find our test dataset
|
241
|
-
test_dataset = next((d for d in datasets if d["name"] == dataset_name), None)
|
242
|
-
assert test_dataset is not None
|
243
|
-
assert test_dataset["row_count"] == 3
|
244
|
-
assert "id" in test_dataset["table_schema"]
|
245
|
-
assert "name" in test_dataset["table_schema"]
|
246
|
-
|
247
|
-
|
248
|
-
async def test_list_datasets_metadata_with_pagination(
|
249
|
-
client: PlanarTestClient,
|
250
|
-
):
|
251
|
-
"""Test the GET /metadata endpoint with pagination parameters."""
|
252
|
-
response = await client.get(
|
253
|
-
"/planar/v1/datasets/metadata",
|
254
|
-
params={"limit": 5, "offset": 0, "schema_name": "main"},
|
255
|
-
)
|
256
|
-
|
257
|
-
assert response.status_code == 200
|
258
|
-
datasets = response.json()
|
259
|
-
assert isinstance(datasets, list)
|
260
|
-
assert len(datasets) <= 5 # Should respect limit
|
261
|
-
|
262
|
-
|
263
|
-
async def test_get_dataset_metadata_endpoint(
|
264
|
-
client: PlanarTestClient,
|
265
|
-
):
|
266
|
-
"""Test the GET /metadata/{dataset_name} endpoint."""
|
267
|
-
dataset_name = "test_single_metadata"
|
268
|
-
dataset = await PlanarDataset.create(dataset_name)
|
269
|
-
|
270
|
-
df = pl.DataFrame(
|
271
|
-
{
|
272
|
-
"id": [1, 2, 3, 4, 5],
|
273
|
-
"value": ["apple", "banana", "cherry", "date", "elderberry"],
|
274
|
-
}
|
275
|
-
)
|
276
|
-
await dataset.write(df)
|
277
|
-
|
278
|
-
response = await client.get(f"/planar/v1/datasets/metadata/{dataset_name}")
|
279
|
-
|
280
|
-
assert response.status_code == 200
|
281
|
-
metadata = response.json()
|
282
|
-
assert metadata["name"] == dataset_name
|
283
|
-
assert metadata["row_count"] == 5
|
284
|
-
assert "id" in metadata["table_schema"]
|
285
|
-
assert "value" in metadata["table_schema"]
|
286
|
-
|
287
|
-
|
288
|
-
async def test_get_dataset_metadata_not_found(
|
289
|
-
client: PlanarTestClient,
|
290
|
-
):
|
291
|
-
"""Test the GET /metadata/{dataset_name} endpoint with non-existent dataset."""
|
292
|
-
response = await client.get("/planar/v1/datasets/metadata/nonexistent_dataset")
|
293
|
-
|
294
|
-
assert response.status_code == 404
|
295
|
-
error = response.json()
|
296
|
-
assert "not found" in error["detail"].lower()
|
297
|
-
|
298
|
-
|
299
|
-
async def test_download_dataset_endpoint(
|
300
|
-
client: PlanarTestClient,
|
301
|
-
):
|
302
|
-
"""Test the GET /content/{dataset_name}/download endpoint."""
|
303
|
-
dataset_name = "test_download"
|
304
|
-
dataset = await PlanarDataset.create(dataset_name)
|
305
|
-
|
306
|
-
df = pl.DataFrame({"id": [1, 2, 3], "value": ["x", "y", "z"]})
|
307
|
-
await dataset.write(df)
|
308
|
-
|
309
|
-
response = await client.get(f"/planar/v1/datasets/content/{dataset_name}/download")
|
310
|
-
|
311
|
-
assert response.status_code == 200
|
312
|
-
assert response.headers["content-type"] == "application/x-parquet"
|
313
|
-
assert f"{dataset_name}.parquet" in response.headers.get("content-disposition", "")
|
314
|
-
|
315
|
-
# Verify we get valid parquet content
|
316
|
-
content = await response.aread()
|
317
|
-
assert len(content) > 0
|
318
|
-
|
319
|
-
# Verify it's valid parquet by reading it back
|
320
|
-
import pyarrow.parquet as pq
|
321
|
-
|
322
|
-
parquet_buffer = pa.py_buffer(content)
|
323
|
-
table = pq.read_table(parquet_buffer)
|
324
|
-
assert table.num_rows == 3
|
325
|
-
assert table.num_columns == 2
|
326
|
-
|
327
|
-
|
328
|
-
async def test_download_dataset_not_found(
|
329
|
-
client: PlanarTestClient,
|
330
|
-
):
|
331
|
-
"""Test the GET /content/{dataset_name}/download endpoint with non-existent dataset."""
|
332
|
-
response = await client.get(
|
333
|
-
"/planar/v1/datasets/content/nonexistent_dataset/download"
|
334
|
-
)
|
335
|
-
|
336
|
-
assert response.status_code == 404
|
337
|
-
error = response.json()
|
338
|
-
assert "not found" in error["detail"].lower()
|
339
|
-
|
340
|
-
|
341
|
-
async def test_stream_arrow_dataset_not_found(
|
342
|
-
client: PlanarTestClient,
|
343
|
-
):
|
344
|
-
"""Test the GET /content/{dataset_name}/arrow-stream endpoint with non-existent dataset."""
|
345
|
-
response = await client.get(
|
346
|
-
"/planar/v1/datasets/content/nonexistent_dataset/arrow-stream"
|
347
|
-
)
|
348
|
-
|
349
|
-
assert response.status_code == 404
|
350
|
-
error = response.json()
|
351
|
-
assert "not found" in error["detail"].lower()
|
352
|
-
|
353
|
-
|
354
|
-
async def test_get_dataset_metadata_empty_dataset(
|
355
|
-
client: PlanarTestClient,
|
356
|
-
):
|
357
|
-
"""Test GET /metadata/{dataset_name} with empty dataset."""
|
358
|
-
dataset_name = "test_empty_metadata"
|
359
|
-
dataset = await PlanarDataset.create(dataset_name)
|
360
|
-
|
361
|
-
# Create empty dataset
|
362
|
-
df = pl.DataFrame(
|
363
|
-
{"id": [], "value": []}, schema={"id": pl.Int64, "value": pl.Utf8}
|
364
|
-
)
|
365
|
-
await dataset.write(df)
|
366
|
-
|
367
|
-
response = await client.get(f"/planar/v1/datasets/metadata/{dataset_name}")
|
368
|
-
assert response.status_code == 200
|
369
|
-
|
370
|
-
metadata = response.json()
|
371
|
-
assert metadata["name"] == dataset_name
|
372
|
-
assert metadata["row_count"] == 0
|
373
|
-
assert "id" in metadata["table_schema"]
|
374
|
-
assert "value" in metadata["table_schema"]
|
375
|
-
|
376
|
-
|
377
|
-
async def test_list_datasets_metadata_empty_dataset(
|
378
|
-
client: PlanarTestClient,
|
379
|
-
):
|
380
|
-
"""Test GET /metadata with empty dataset in the list."""
|
381
|
-
dataset_name = "test_empty_in_list"
|
382
|
-
dataset = await PlanarDataset.create(dataset_name)
|
383
|
-
|
384
|
-
# Create empty dataset
|
385
|
-
df = pl.DataFrame(
|
386
|
-
{"id": [], "value": []}, schema={"id": pl.Int64, "value": pl.Utf8}
|
387
|
-
)
|
388
|
-
await dataset.write(df)
|
389
|
-
|
390
|
-
response = await client.get("/planar/v1/datasets/metadata")
|
391
|
-
assert response.status_code == 200
|
392
|
-
|
393
|
-
datasets = response.json()
|
394
|
-
empty_dataset = next((d for d in datasets if d["name"] == dataset_name), None)
|
395
|
-
assert empty_dataset is not None
|
396
|
-
assert empty_dataset["row_count"] == 0
|
397
|
-
|
398
|
-
|
399
|
-
async def test_download_empty_dataset(
|
400
|
-
client: PlanarTestClient,
|
401
|
-
):
|
402
|
-
"""Test GET /content/{dataset_name}/download with empty dataset."""
|
403
|
-
dataset_name = "test_empty_download"
|
404
|
-
dataset = await PlanarDataset.create(dataset_name)
|
405
|
-
|
406
|
-
# Create empty dataset
|
407
|
-
df = pl.DataFrame(
|
408
|
-
{"id": [], "value": []}, schema={"id": pl.Int64, "value": pl.Utf8}
|
409
|
-
)
|
410
|
-
await dataset.write(df)
|
411
|
-
|
412
|
-
response = await client.get(f"/planar/v1/datasets/content/{dataset_name}/download")
|
413
|
-
assert response.status_code == 200
|
414
|
-
assert response.headers["content-type"] == "application/x-parquet"
|
415
|
-
assert f"{dataset_name}.parquet" in response.headers.get("content-disposition", "")
|
416
|
-
|
417
|
-
# Verify we get valid parquet content (even if empty)
|
418
|
-
content = await response.aread()
|
419
|
-
assert len(content) > 0 # Should have parquet metadata even for empty data
|
420
|
-
|
421
|
-
# Verify it's valid parquet by reading it back
|
422
|
-
import pyarrow.parquet as pq
|
423
|
-
|
424
|
-
parquet_buffer = pa.py_buffer(content)
|
425
|
-
table = pq.read_table(parquet_buffer)
|
426
|
-
assert table.num_rows == 0
|
427
|
-
assert table.num_columns == 2 # id and value columns
|
428
|
-
assert table.schema.field("id").type == pa.int64()
|
429
|
-
assert table.schema.field("value").type == pa.string()
|
@@ -1,49 +0,0 @@
|
|
1
|
-
import io
|
2
|
-
from uuid import UUID
|
3
|
-
|
4
|
-
import pytest
|
5
|
-
from sqlmodel.ext.asyncio.session import AsyncSession
|
6
|
-
|
7
|
-
from planar import PlanarApp, sqlite_config
|
8
|
-
from planar.files.models import PlanarFileMetadata
|
9
|
-
from planar.testing.planar_test_client import PlanarTestClient
|
10
|
-
|
11
|
-
|
12
|
-
@pytest.fixture(name="app")
|
13
|
-
def app_fixture(tmp_db_path: str):
|
14
|
-
return PlanarApp(
|
15
|
-
config=sqlite_config(tmp_db_path),
|
16
|
-
title="Test app for files router",
|
17
|
-
description="Testing files endpoints",
|
18
|
-
)
|
19
|
-
|
20
|
-
|
21
|
-
async def test_upload_parquet_sets_content_type(
|
22
|
-
client: PlanarTestClient, session: AsyncSession
|
23
|
-
):
|
24
|
-
"""Uploading a .parquet file should persist application/x-parquet in metadata."""
|
25
|
-
|
26
|
-
# Prepare a small in-memory payload and intentionally send an octet-stream
|
27
|
-
# to simulate browsers that don't know parquet. The route should override
|
28
|
-
# this using mimetypes.guess_type.
|
29
|
-
filename = "test_data.parquet"
|
30
|
-
payload = b"PAR1" # content doesn't matter for MIME guessing by filename
|
31
|
-
|
32
|
-
files = {
|
33
|
-
"files": (filename, io.BytesIO(payload), "application/octet-stream"),
|
34
|
-
}
|
35
|
-
|
36
|
-
resp = await client.post("/planar/v1/file/upload", files=files)
|
37
|
-
assert resp.status_code == 200
|
38
|
-
|
39
|
-
body = resp.json()
|
40
|
-
assert isinstance(body, list) and len(body) == 1
|
41
|
-
file_item = body[0]
|
42
|
-
assert file_item["filename"] == filename
|
43
|
-
|
44
|
-
# Verify the database record has the correct MIME type
|
45
|
-
file_id = UUID(file_item["id"])
|
46
|
-
meta = await session.get(PlanarFileMetadata, file_id)
|
47
|
-
|
48
|
-
assert meta is not None
|
49
|
-
assert meta.content_type == "application/x-parquet"
|