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.
Files changed (73) hide show
  1. planar/app.py +26 -6
  2. planar/cli.py +26 -0
  3. planar/data/__init__.py +1 -0
  4. planar/data/config.py +12 -1
  5. planar/data/connection.py +89 -4
  6. planar/data/dataset.py +13 -7
  7. planar/data/utils.py +145 -25
  8. planar/db/alembic/env.py +68 -57
  9. planar/db/alembic.ini +1 -1
  10. planar/files/storage/config.py +7 -1
  11. planar/routers/dataset_router.py +5 -1
  12. planar/routers/info.py +79 -36
  13. planar/scaffold_templates/pyproject.toml.j2 +1 -1
  14. planar/testing/fixtures.py +7 -4
  15. planar/testing/planar_test_client.py +8 -0
  16. planar/version.py +27 -0
  17. planar-0.12.0.dist-info/METADATA +202 -0
  18. {planar-0.10.0.dist-info → planar-0.12.0.dist-info}/RECORD +20 -71
  19. planar/ai/test_agent_serialization.py +0 -229
  20. planar/ai/test_agent_tool_step_display.py +0 -78
  21. planar/data/test_dataset.py +0 -358
  22. planar/files/storage/test_azure_blob.py +0 -435
  23. planar/files/storage/test_local_directory.py +0 -162
  24. planar/files/storage/test_s3.py +0 -299
  25. planar/files/test_files.py +0 -282
  26. planar/human/test_human.py +0 -385
  27. planar/logging/test_formatter.py +0 -327
  28. planar/modeling/mixins/test_auditable.py +0 -97
  29. planar/modeling/mixins/test_timestamp.py +0 -134
  30. planar/modeling/mixins/test_uuid_primary_key.py +0 -52
  31. planar/routers/test_agents_router.py +0 -174
  32. planar/routers/test_dataset_router.py +0 -429
  33. planar/routers/test_files_router.py +0 -49
  34. planar/routers/test_object_config_router.py +0 -367
  35. planar/routers/test_routes_security.py +0 -168
  36. planar/routers/test_rule_router.py +0 -470
  37. planar/routers/test_workflow_router.py +0 -564
  38. planar/rules/test_data/account_dormancy_management.json +0 -223
  39. planar/rules/test_data/airline_loyalty_points_calculator.json +0 -262
  40. planar/rules/test_data/applicant_risk_assessment.json +0 -435
  41. planar/rules/test_data/booking_fraud_detection.json +0 -407
  42. planar/rules/test_data/cellular_data_rollover_system.json +0 -258
  43. planar/rules/test_data/clinical_trial_eligibility_screener.json +0 -437
  44. planar/rules/test_data/customer_lifetime_value.json +0 -143
  45. planar/rules/test_data/import_duties_calculator.json +0 -289
  46. planar/rules/test_data/insurance_prior_authorization.json +0 -443
  47. planar/rules/test_data/online_check_in_eligibility_system.json +0 -254
  48. planar/rules/test_data/order_consolidation_system.json +0 -375
  49. planar/rules/test_data/portfolio_risk_monitor.json +0 -471
  50. planar/rules/test_data/supply_chain_risk.json +0 -253
  51. planar/rules/test_data/warehouse_cross_docking.json +0 -237
  52. planar/rules/test_rules.py +0 -1494
  53. planar/security/tests/test_auth_middleware.py +0 -162
  54. planar/security/tests/test_authorization_context.py +0 -78
  55. planar/security/tests/test_cedar_basics.py +0 -41
  56. planar/security/tests/test_cedar_policies.py +0 -158
  57. planar/security/tests/test_jwt_principal_context.py +0 -179
  58. planar/test_app.py +0 -142
  59. planar/test_cli.py +0 -394
  60. planar/test_config.py +0 -515
  61. planar/test_object_config.py +0 -527
  62. planar/test_object_registry.py +0 -14
  63. planar/test_sqlalchemy.py +0 -193
  64. planar/test_utils.py +0 -105
  65. planar/testing/test_memory_storage.py +0 -143
  66. planar/workflows/test_concurrency_detection.py +0 -120
  67. planar/workflows/test_lock_timeout.py +0 -140
  68. planar/workflows/test_serialization.py +0 -1203
  69. planar/workflows/test_suspend_deserialization.py +0 -231
  70. planar/workflows/test_workflow.py +0 -2005
  71. planar-0.10.0.dist-info/METADATA +0 -323
  72. {planar-0.10.0.dist-info → planar-0.12.0.dist-info}/WHEEL +0 -0
  73. {planar-0.10.0.dist-info → planar-0.12.0.dist-info}/entry_points.txt +0 -0
@@ -1,367 +0,0 @@
1
- """
2
- Tests for object configuration router endpoints.
3
-
4
- This module tests the object configuration router endpoints to ensure they work correctly
5
- for both agent and rule configurations.
6
- """
7
-
8
- from typing import AsyncGenerator
9
- from uuid import UUID, uuid4
10
-
11
- import pytest
12
- from pydantic import BaseModel, Field
13
- from sqlmodel.ext.asyncio.session import AsyncSession
14
-
15
- from planar.ai.agent import Agent
16
- from planar.ai.agent_utils import agent_configuration
17
- from planar.ai.models import AgentConfig
18
- from planar.app import PlanarApp
19
- from planar.config import sqlite_config
20
- from planar.object_config import DEFAULT_UUID, ObjectConfiguration
21
- from planar.object_config.object_config import ConfigurableObjectType
22
- from planar.object_registry import ObjectRegistry
23
- from planar.rules.decorator import rule
24
- from planar.rules.models import Rule, RuleEngineConfig, create_jdm_graph
25
- from planar.rules.rule_configuration import rule_configuration
26
- from planar.testing.planar_test_client import PlanarTestClient
27
-
28
-
29
- class InputForTestRule(BaseModel):
30
- """Input for test rule."""
31
-
32
- value: int = Field(description="Test value")
33
- category: str = Field(description="Test category")
34
-
35
-
36
- class OutputFromTestRule(BaseModel):
37
- """Output from test rule."""
38
-
39
- result: int = Field(description="Result value")
40
- message: str = Field(description="Result message")
41
-
42
-
43
- @pytest.fixture(name="app")
44
- def app_fixture(tmp_db_path: str):
45
- """Create a test app with agents and rules."""
46
- app = PlanarApp(
47
- config=sqlite_config(tmp_db_path),
48
- title="Test app for object config router",
49
- description="Testing object configuration endpoints",
50
- )
51
-
52
- # Register a simple agent
53
- simple_agent = Agent(
54
- name="test_agent",
55
- system_prompt="Test system prompt",
56
- user_prompt="Test user prompt: {input}",
57
- model="openai:gpt-4o",
58
- max_turns=2,
59
- )
60
- app.register_agent(simple_agent)
61
-
62
- # Create and register a rule
63
- @rule(description="Test rule for configuration")
64
- def test_rule(input: InputForTestRule) -> OutputFromTestRule:
65
- # Default implementation
66
- return OutputFromTestRule(
67
- result=input.value * 2, message=f"Processed {input.category}"
68
- )
69
-
70
- app.register_rule(test_rule)
71
-
72
- return app
73
-
74
-
75
- @pytest.fixture
76
- async def agent_with_configs(app: PlanarApp, session: AsyncSession):
77
- """Create an agent with multiple configurations."""
78
- # First config
79
- agent_config_1 = AgentConfig(
80
- system_prompt="Config 1 system",
81
- user_prompt="Config 1 user: {input}",
82
- model="openai:gpt-4o",
83
- max_turns=3,
84
- )
85
- await agent_configuration.write_config("test_agent", agent_config_1)
86
-
87
- # Second config
88
- agent_config_2 = AgentConfig(
89
- system_prompt="Config 2 system",
90
- user_prompt="Config 2 user: {input}",
91
- model="anthropic:claude-3-sonnet",
92
- max_turns=5,
93
- )
94
- config_2 = await agent_configuration.write_config("test_agent", agent_config_2)
95
-
96
- # Make the second config active
97
- await agent_configuration.promote_config(config_2.id)
98
-
99
- return config_2.id
100
-
101
-
102
- @pytest.fixture
103
- async def rule_with_configs(
104
- session: AsyncSession,
105
- ) -> AsyncGenerator[tuple[Rule, list[ObjectConfiguration]], None]:
106
- class RuleInputOutput(BaseModel):
107
- test: str
108
-
109
- rule = Rule(
110
- name=f"test_rule_promote_{uuid4().hex}",
111
- description="Test rule for promoting configuration",
112
- input=RuleInputOutput,
113
- output=RuleInputOutput,
114
- )
115
- ObjectRegistry.get_instance().register(rule)
116
-
117
- # Create some configs
118
- jdm_config_1 = create_jdm_graph(rule)
119
- jdm_config_2 = create_jdm_graph(rule)
120
-
121
- rule_config_1 = RuleEngineConfig(jdm=jdm_config_1)
122
- rule_config_2 = RuleEngineConfig(jdm=jdm_config_2)
123
-
124
- config1 = await rule_configuration.write_config(rule.name, rule_config_1)
125
- config2 = await rule_configuration.write_config(rule.name, rule_config_2)
126
-
127
- yield rule, [config1, config2]
128
-
129
-
130
- async def test_promote_agent_config(
131
- client: PlanarTestClient,
132
- app: PlanarApp,
133
- session: AsyncSession,
134
- agent_with_configs: UUID,
135
- ):
136
- """Test promoting an agent configuration."""
137
- # Get the configurations first to find a non-active one
138
- agent = app._object_registry.get_agents()[0]
139
- configs = await agent_configuration.read_configs_with_default(
140
- "test_agent", agent.to_config()
141
- )
142
-
143
- # Find the first (inactive) config
144
- inactive_config = next(c for c in configs if not c.active)
145
-
146
- # Promote the inactive config
147
- request_data = {
148
- "object_type": ConfigurableObjectType.AGENT,
149
- "config_id": str(inactive_config.id),
150
- "object_name": "test_agent",
151
- }
152
-
153
- response = await client.post(
154
- "/planar/v1/object-configurations/promote", json=request_data
155
- )
156
- assert response.status_code == 200
157
-
158
- result = response.json()
159
- assert "configs" in result
160
- assert len(result["configs"]) >= 3 # At least 2 configs + default
161
-
162
- # Verify the promoted config is now active
163
- promoted_config = next(
164
- c for c in result["configs"] if c["id"] == str(inactive_config.id)
165
- )
166
- assert promoted_config["active"] is True
167
-
168
- # Verify other configs are inactive
169
- for config in result["configs"]:
170
- if config["id"] != str(inactive_config.id):
171
- assert config["active"] is False
172
-
173
-
174
- async def test_promote_rule_config(
175
- client: PlanarTestClient,
176
- app: PlanarApp,
177
- session: AsyncSession,
178
- ):
179
- """Test promoting a rule configuration."""
180
- # Get the configurations first to find a non-active one
181
- rule = ObjectRegistry.get_instance().get_rules()[0]
182
-
183
- await rule_configuration.write_config(
184
- rule.name, RuleEngineConfig(jdm=create_jdm_graph(rule))
185
- )
186
-
187
- configs = await rule_configuration.read_configs_with_default(
188
- rule.name, rule.to_config()
189
- )
190
-
191
- assert len(configs) == 2
192
-
193
- # Find the first (inactive) config
194
- inactive_config = next(c for c in configs if not c.active)
195
-
196
- # Promote the inactive config
197
- request_data = {
198
- "object_type": ConfigurableObjectType.RULE,
199
- "config_id": str(inactive_config.id),
200
- "object_name": rule.name,
201
- }
202
-
203
- response = await client.post(
204
- "/planar/v1/object-configurations/promote", json=request_data
205
- )
206
- assert response.status_code == 200
207
-
208
- result = response.json()
209
- assert "configs" in result
210
- assert len(result["configs"]) == 2
211
-
212
- # Verify the promoted config is now active
213
- promoted_config = next(
214
- c for c in result["configs"] if c["id"] == str(inactive_config.id)
215
- )
216
- assert promoted_config["active"] is True
217
-
218
- # Verify the config data is correct
219
- assert promoted_config["object_type"] == "rule"
220
- assert promoted_config["object_name"] == rule.name
221
- assert "jdm" in promoted_config["data"]
222
-
223
-
224
- async def test_promote_to_default_agent(
225
- client: PlanarTestClient,
226
- app: PlanarApp,
227
- session: AsyncSession,
228
- agent_with_configs: UUID,
229
- ):
230
- """Test promoting to default (revert to original implementation) for agent."""
231
- # Promote to default using the special UUID
232
- request_data = {
233
- "object_type": ConfigurableObjectType.AGENT,
234
- "config_id": str(DEFAULT_UUID),
235
- "object_name": "test_agent",
236
- }
237
-
238
- response = await client.post(
239
- "/planar/v1/object-configurations/promote", json=request_data
240
- )
241
- assert response.status_code == 200
242
-
243
- result = response.json()
244
- assert "configs" in result
245
-
246
- # Verify all non-default configs are inactive
247
- for config in result["configs"]:
248
- if config["version"] == 0: # Default config
249
- assert config["active"] is True
250
- else:
251
- assert config["active"] is False
252
-
253
-
254
- async def test_promote_to_default_rule(
255
- client: PlanarTestClient,
256
- app: PlanarApp,
257
- session: AsyncSession,
258
- rule_with_configs: UUID,
259
- ):
260
- """Test promoting to default (revert to original implementation) for rule."""
261
- # Promote to default using the special UUID
262
- request_data = {
263
- "object_type": ConfigurableObjectType.RULE,
264
- "config_id": str(DEFAULT_UUID),
265
- "object_name": "test_rule",
266
- }
267
-
268
- response = await client.post(
269
- "/planar/v1/object-configurations/promote", json=request_data
270
- )
271
- assert response.status_code == 200
272
-
273
- result = response.json()
274
- assert "configs" in result
275
-
276
- # Verify all non-default configs are inactive
277
- for config in result["configs"]:
278
- if config["version"] == 0: # Default config
279
- assert config["active"] is True
280
- else:
281
- assert config["active"] is False
282
-
283
-
284
- async def test_promote_nonexistent_agent(
285
- client: PlanarTestClient, app: PlanarApp, session: AsyncSession
286
- ):
287
- """Test promoting config for non-existent agent."""
288
- request_data = {
289
- "object_type": ConfigurableObjectType.AGENT,
290
- "config_id": str(UUID("12345678-1234-5678-1234-567812345678")),
291
- "object_name": "nonexistent_agent",
292
- }
293
-
294
- response = await client.post(
295
- "/planar/v1/object-configurations/promote", json=request_data
296
- )
297
- assert response.status_code == 404
298
- assert response.json()["detail"] == "Agent not found"
299
-
300
-
301
- async def test_promote_nonexistent_rule(
302
- client: PlanarTestClient, app: PlanarApp, session: AsyncSession
303
- ):
304
- """Test promoting config for non-existent rule."""
305
- request_data = {
306
- "object_type": ConfigurableObjectType.RULE,
307
- "config_id": str(UUID("12345678-1234-5678-1234-567812345678")),
308
- "object_name": "nonexistent_rule",
309
- }
310
-
311
- response = await client.post(
312
- "/planar/v1/object-configurations/promote", json=request_data
313
- )
314
- assert response.status_code == 404
315
- assert response.json()["detail"] == "Rule not found"
316
-
317
-
318
- async def test_promote_nonexistent_config(
319
- client: PlanarTestClient,
320
- app: PlanarApp,
321
- session: AsyncSession,
322
- agent_with_configs: UUID,
323
- ):
324
- """Test promoting a non-existent configuration."""
325
- # Try to promote a config that doesn't exist
326
- request_data = {
327
- "object_type": ConfigurableObjectType.AGENT,
328
- "config_id": str(UUID("99999999-9999-9999-9999-999999999999")),
329
- "object_name": "test_agent",
330
- }
331
-
332
- # This should fail with an error from the promote_config method
333
- response = await client.post(
334
- "/planar/v1/object-configurations/promote", json=request_data
335
- )
336
- assert response.status_code == 404
337
-
338
-
339
- async def test_config_versions_ordering(
340
- client: PlanarTestClient,
341
- app: PlanarApp,
342
- session: AsyncSession,
343
- agent_with_configs: UUID,
344
- ):
345
- """Test that configurations are returned in correct version order."""
346
- # Promote to ensure we have a known state
347
- request_data = {
348
- "object_type": ConfigurableObjectType.AGENT,
349
- "config_id": str(agent_with_configs),
350
- "object_name": "test_agent",
351
- }
352
-
353
- response = await client.post(
354
- "/planar/v1/object-configurations/promote", json=request_data
355
- )
356
- assert response.status_code == 200
357
-
358
- result = response.json()
359
- configs = result["configs"]
360
-
361
- # Verify configs are ordered by version descending (except default which is always last)
362
- non_default_configs = [c for c in configs if c["version"] != 0]
363
- versions = [c["version"] for c in non_default_configs]
364
- assert versions == sorted(versions, reverse=True)
365
-
366
- # Verify default config is last
367
- assert configs[-1]["version"] == 0
@@ -1,168 +0,0 @@
1
- from http import HTTPStatus
2
-
3
- import pytest
4
-
5
- from planar import PlanarApp, sqlite_config
6
- from planar.config import AuthzConfig, SecurityConfig
7
- from planar.security.auth_context import Principal, clear_principal, set_principal
8
- from planar.testing.planar_test_client import PlanarTestClient
9
- from planar.workflows import workflow
10
-
11
-
12
- # ------ TEST SETUP ------
13
- @workflow()
14
- async def simple_test_workflow(test_id: str) -> str:
15
- """
16
- Simpleorkflow that returns the test id
17
- """
18
- return test_id
19
-
20
-
21
- @pytest.fixture(name="app_with_no_authz")
22
- def create_app_no_authz():
23
- config = sqlite_config("test_authz_router.db")
24
-
25
- return PlanarApp(
26
- config=config,
27
- title="Test Authorization in Router",
28
- description="API for testing workflow routers",
29
- ).register_workflow(simple_test_workflow)
30
-
31
-
32
- @pytest.fixture(name="app_with_default_authz")
33
- def create_app_with_authz():
34
- config = sqlite_config("test_authz_router.db")
35
- config.security = SecurityConfig(authz=AuthzConfig(enabled=True, policy_file=None))
36
-
37
- return PlanarApp(
38
- config=config,
39
- title="Test Authorization in Router",
40
- description="API for testing workflow routers",
41
- ).register_workflow(simple_test_workflow)
42
-
43
-
44
- @pytest.fixture
45
- def restrictive_policy_file(tmp_path):
46
- """Create a restrictive policy file for testing."""
47
- policy_content = """
48
- // Only allow Workflow::List actions when role is admin
49
- permit (
50
- principal,
51
- action == Action::"Workflow::List",
52
- resource
53
- ) when {
54
- principal.role == "admin"
55
- };
56
-
57
- """
58
- policy_file = tmp_path / "restrictive_policies.cedar"
59
- policy_file.write_text(policy_content)
60
- return str(policy_file)
61
-
62
-
63
- @pytest.fixture(name="app_with_restricted_authz")
64
- def create_app_with_restricted_authz(tmp_path, restrictive_policy_file):
65
- db_path = tmp_path / "test_authz_router.db"
66
- config = sqlite_config(str(db_path))
67
- config.security = SecurityConfig(
68
- authz=AuthzConfig(enabled=True, policy_file=restrictive_policy_file)
69
- )
70
-
71
- return PlanarApp(
72
- config=config,
73
- title="Test Authorization in Router",
74
- description="API for testing workflow routers",
75
- ).register_workflow(simple_test_workflow)
76
-
77
-
78
- # ------ TESTS ------
79
-
80
-
81
- def assert_workflow_list(response):
82
- # Verify the response status code
83
- assert response.status_code == 200
84
-
85
- # Parse the response data
86
- data = response.json()
87
-
88
- # Verify that two workflows are returned
89
- assert data["total"] == 1
90
- assert len(data["items"]) == 1
91
-
92
- assert data["offset"] == 0
93
- assert data["limit"] == 10
94
-
95
- # Verify the expense workflow details
96
- simple_test_workflow = next(
97
- item for item in data["items"] if item["name"] == "simple_test_workflow"
98
- )
99
- assert simple_test_workflow["fully_qualified_name"] == "simple_test_workflow"
100
-
101
- # # Verify that the workflows have input and output schemas
102
- assert "input_schema" in simple_test_workflow
103
- assert "output_schema" in simple_test_workflow
104
-
105
-
106
- async def test_list_workflows_no_authz(app_with_no_authz):
107
- """
108
- Test that the workflow management router correctly lists registered workflows.
109
- """
110
-
111
- async with app_with_no_authz._lifespan(app_with_no_authz.fastapi):
112
- client = PlanarTestClient(app_with_no_authz)
113
- # Call the workflow management endpoint to list workflows
114
- response = await client.get("/planar/v1/workflows/")
115
- assert_workflow_list(response)
116
-
117
-
118
- async def test_list_workflows_with_default_authz(app_with_default_authz):
119
- """
120
- Test that the workflow management router correctly lists registered workflows when authorization is enabled but no policy file is provided.
121
- """
122
-
123
- async with app_with_default_authz._lifespan(app_with_default_authz.fastapi):
124
- client = PlanarTestClient(app_with_default_authz)
125
- principal = Principal(sub="test_user") # type: ignore
126
- token = set_principal(principal)
127
-
128
- # Call the workflow management endpoint to list workflows
129
- response = await client.get("/planar/v1/workflows/")
130
- assert_workflow_list(response)
131
-
132
- clear_principal(token)
133
-
134
-
135
- async def test_list_workflows_with_restricted_authz(app_with_restricted_authz):
136
- """
137
- Test that the workflow management router correctly lists registered workflows when authorization is enabled and a policy file is provided.
138
- """
139
-
140
- async with app_with_restricted_authz._lifespan(app_with_restricted_authz.fastapi):
141
- client = PlanarTestClient(app_with_restricted_authz)
142
- principal = Principal(sub="test_user", role="admin") # type: ignore
143
- token = set_principal(principal)
144
-
145
- # Call the workflow management endpoint to list workflows
146
- response = await client.get("/planar/v1/workflows/")
147
- assert_workflow_list(response)
148
-
149
- clear_principal(token)
150
-
151
-
152
- async def test_list_workflows_with_restricted_authz_and_wrong_role(
153
- app_with_restricted_authz,
154
- ):
155
- """
156
- Test that the workflow management router correctly forbids access to workflows list.
157
- """
158
-
159
- async with app_with_restricted_authz._lifespan(app_with_restricted_authz.fastapi):
160
- client = PlanarTestClient(app_with_restricted_authz)
161
- principal = Principal(sub="test_user", role="test_role") # type: ignore
162
- token = set_principal(principal)
163
-
164
- # Call the workflow management endpoint to list workflows
165
- response = await client.get("/planar/v1/workflows/")
166
- assert response.status_code == HTTPStatus.FORBIDDEN
167
-
168
- clear_principal(token)