planar 0.10.0__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 (60) hide show
  1. planar/app.py +18 -6
  2. planar/routers/info.py +79 -36
  3. planar/scaffold_templates/pyproject.toml.j2 +1 -1
  4. planar/testing/fixtures.py +7 -4
  5. {planar-0.10.0.dist-info → planar-0.11.0.dist-info}/METADATA +9 -1
  6. {planar-0.10.0.dist-info → planar-0.11.0.dist-info}/RECORD +8 -60
  7. planar/ai/test_agent_serialization.py +0 -229
  8. planar/ai/test_agent_tool_step_display.py +0 -78
  9. planar/data/test_dataset.py +0 -358
  10. planar/files/storage/test_azure_blob.py +0 -435
  11. planar/files/storage/test_local_directory.py +0 -162
  12. planar/files/storage/test_s3.py +0 -299
  13. planar/files/test_files.py +0 -282
  14. planar/human/test_human.py +0 -385
  15. planar/logging/test_formatter.py +0 -327
  16. planar/modeling/mixins/test_auditable.py +0 -97
  17. planar/modeling/mixins/test_timestamp.py +0 -134
  18. planar/modeling/mixins/test_uuid_primary_key.py +0 -52
  19. planar/routers/test_agents_router.py +0 -174
  20. planar/routers/test_dataset_router.py +0 -429
  21. planar/routers/test_files_router.py +0 -49
  22. planar/routers/test_object_config_router.py +0 -367
  23. planar/routers/test_routes_security.py +0 -168
  24. planar/routers/test_rule_router.py +0 -470
  25. planar/routers/test_workflow_router.py +0 -564
  26. planar/rules/test_data/account_dormancy_management.json +0 -223
  27. planar/rules/test_data/airline_loyalty_points_calculator.json +0 -262
  28. planar/rules/test_data/applicant_risk_assessment.json +0 -435
  29. planar/rules/test_data/booking_fraud_detection.json +0 -407
  30. planar/rules/test_data/cellular_data_rollover_system.json +0 -258
  31. planar/rules/test_data/clinical_trial_eligibility_screener.json +0 -437
  32. planar/rules/test_data/customer_lifetime_value.json +0 -143
  33. planar/rules/test_data/import_duties_calculator.json +0 -289
  34. planar/rules/test_data/insurance_prior_authorization.json +0 -443
  35. planar/rules/test_data/online_check_in_eligibility_system.json +0 -254
  36. planar/rules/test_data/order_consolidation_system.json +0 -375
  37. planar/rules/test_data/portfolio_risk_monitor.json +0 -471
  38. planar/rules/test_data/supply_chain_risk.json +0 -253
  39. planar/rules/test_data/warehouse_cross_docking.json +0 -237
  40. planar/rules/test_rules.py +0 -1494
  41. planar/security/tests/test_auth_middleware.py +0 -162
  42. planar/security/tests/test_authorization_context.py +0 -78
  43. planar/security/tests/test_cedar_basics.py +0 -41
  44. planar/security/tests/test_cedar_policies.py +0 -158
  45. planar/security/tests/test_jwt_principal_context.py +0 -179
  46. planar/test_app.py +0 -142
  47. planar/test_cli.py +0 -394
  48. planar/test_config.py +0 -515
  49. planar/test_object_config.py +0 -527
  50. planar/test_object_registry.py +0 -14
  51. planar/test_sqlalchemy.py +0 -193
  52. planar/test_utils.py +0 -105
  53. planar/testing/test_memory_storage.py +0 -143
  54. planar/workflows/test_concurrency_detection.py +0 -120
  55. planar/workflows/test_lock_timeout.py +0 -140
  56. planar/workflows/test_serialization.py +0 -1203
  57. planar/workflows/test_suspend_deserialization.py +0 -231
  58. planar/workflows/test_workflow.py +0 -2005
  59. {planar-0.10.0.dist-info → planar-0.11.0.dist-info}/WHEEL +0 -0
  60. {planar-0.10.0.dist-info → planar-0.11.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)