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,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)
@@ -1,470 +0,0 @@
1
- from decimal import Decimal
2
-
3
- import pytest
4
- from pydantic import BaseModel
5
-
6
- from planar.app import PlanarApp
7
- from planar.config import sqlite_config
8
- from planar.rules import rule
9
- from planar.testing.planar_test_client import PlanarTestClient
10
-
11
-
12
- class ExpenseRuleInput(BaseModel):
13
- title: str
14
- amount: float
15
- description: str
16
- status: str
17
- category: str
18
-
19
-
20
- class RuleOutput(BaseModel):
21
- reason: str
22
- approved: bool
23
-
24
-
25
- class TransactionVolumeRow(BaseModel):
26
- period: str
27
- country: str
28
- currency: str
29
- completed_count: int
30
- rejected_count: int
31
-
32
-
33
- class TransactionVolume(BaseModel):
34
- rows: list[TransactionVolumeRow]
35
- total_completed_count: int
36
- total_rejected_count: int
37
-
38
-
39
- class PricingInput(BaseModel):
40
- rows: list[TransactionVolumeRow]
41
-
42
-
43
- class TransactionPricingLine(TransactionVolumeRow):
44
- completed_price_per_transaction_usd: Decimal
45
- rejected_price_per_transaction_usd: Decimal
46
-
47
-
48
- class PricingRuleOutput(BaseModel):
49
- line_items: list[TransactionPricingLine]
50
-
51
-
52
- class PricingRuleOutputWrongType(BaseModel):
53
- line_items: list[TransactionPricingLine]
54
- some_other_field: str
55
-
56
-
57
- @rule(description="Complex business rule")
58
- def complex_business_rule(input: ExpenseRuleInput) -> RuleOutput:
59
- """
60
- A complex business rule that determines if the expense should be approved
61
- """
62
- # input and output must be json serializable objects for the zen / gorules lib to work
63
- return RuleOutput(reason="The widgets look fantastic", approved=True)
64
-
65
-
66
- @rule(description="Calculates fees based on tiered, total transaction volume.")
67
- def pricing_rule(
68
- input: PricingInput,
69
- ) -> PricingRuleOutput:
70
- """
71
- Calculates fees based on country, currency, and tiered volume.
72
- """
73
- return PricingRuleOutput(line_items=[])
74
-
75
-
76
- @rule(
77
- description="Calculates fees based on tiered, total transaction volume with wrong type"
78
- )
79
- def pricing_rule_with_wrong_type(
80
- input: PricingInput,
81
- ) -> PricingRuleOutputWrongType:
82
- return PricingRuleOutputWrongType(line_items=[], some_other_field="test")
83
-
84
-
85
- @pytest.fixture(name="app")
86
- def app_fixture(tmp_db_path: str):
87
- app = PlanarApp(
88
- config=sqlite_config(tmp_db_path),
89
- title="Test app for agent router",
90
- description="Testing agent endpoints",
91
- )
92
-
93
- app.register_rule(complex_business_rule)
94
- app.register_rule(pricing_rule_with_wrong_type)
95
- app.register_rule(pricing_rule)
96
- return app
97
-
98
-
99
- EXPENSE_RULE_JDM = {
100
- "nodes": [
101
- {
102
- "id": "7e51efb8-7463-4775-ad69-180442a34444",
103
- "type": "inputNode",
104
- "name": "Input",
105
- "content": {
106
- "schema": '{"properties": {"title": {"title": "Title", "type": "string"}, "amount": {"title": "Amount", "type": "number"}, "description": {"title": "Description", "type": "string"}, "status": {"title": "Status", "type": "string"}, "category": {"title": "Category", "type": "string"}}, "required": ["title", "amount", "description", "status", "category"], "title": "ExpenseRuleInput", "type": "object"}'
107
- },
108
- "position": {"x": 100, "y": 100},
109
- },
110
- {
111
- "id": "abf8c265-da42-4b81-b7bf-349d3e248294",
112
- "type": "decisionTableNode",
113
- "name": "decisionTable1",
114
- "content": {
115
- "hitPolicy": "first",
116
- "rules": [
117
- {
118
- "_id": "9fc59e78-58be-412b-9d2b-79c22bcfefe4",
119
- "2a5ac809-24db-431b-a228-7bc318cd0a3f": "",
120
- "25f2e0b6-c02b-43a2-a9cd-d40e5c1ca709": '"default value"',
121
- "4b09e532-bb42-453b-9c00-26af89f70a03": "true",
122
- }
123
- ],
124
- "inputs": [
125
- {
126
- "id": "2a5ac809-24db-431b-a228-7bc318cd0a3f",
127
- "name": "Input",
128
- "field": "",
129
- }
130
- ],
131
- "outputs": [
132
- {
133
- "id": "25f2e0b6-c02b-43a2-a9cd-d40e5c1ca709",
134
- "field": "reason",
135
- "name": "reason",
136
- },
137
- {
138
- "id": "4b09e532-bb42-453b-9c00-26af89f70a03",
139
- "field": "approved",
140
- "name": "approved",
141
- },
142
- ],
143
- "passThrough": True,
144
- "passThorough": False,
145
- "inputField": None,
146
- "outputPath": None,
147
- "executionMode": "single",
148
- },
149
- "position": {"x": 405, "y": 120},
150
- },
151
- {
152
- "id": "40abc689-6e0e-40ee-bc76-df51065e6ff5",
153
- "type": "outputNode",
154
- "name": "Output",
155
- "content": {
156
- "schema": '{"properties": {"reason": {"title": "Reason", "type": "string"}, "approved": {"title": "Approved", "type": "boolean"}}, "required": ["reason", "approved"], "title": "RuleOutput", "type": "object"}'
157
- },
158
- "position": {"x": 885, "y": 130},
159
- },
160
- {
161
- "id": "a5385f35-5ba7-4cbf-a5b8-f87bca6fd95c",
162
- "type": "expressionNode",
163
- "name": "expression1",
164
- "content": {
165
- "expressions": [],
166
- "passThrough": True,
167
- "inputField": None,
168
- "outputPath": None,
169
- "executionMode": "single",
170
- },
171
- "position": {"x": 590, "y": 350},
172
- },
173
- {
174
- "id": "0689381e-0650-4ba5-b4ba-1a1800f035ca",
175
- "type": "decisionTableNode",
176
- "name": "decisionTable2",
177
- "content": {
178
- "hitPolicy": "first",
179
- "rules": [],
180
- "inputs": [
181
- {
182
- "id": "4caf4578-a643-4a3c-bc6e-2bdf8559e601",
183
- "name": "Input",
184
- "field": "",
185
- }
186
- ],
187
- "outputs": [
188
- {
189
- "id": "4ec07a83-70ca-46ec-aee7-331c44a8da76",
190
- "field": "output",
191
- "name": "Output",
192
- }
193
- ],
194
- "passThrough": True,
195
- "passThorough": None,
196
- "inputField": None,
197
- "outputPath": None,
198
- "executionMode": "single",
199
- },
200
- "position": {"x": 885, "y": 500},
201
- },
202
- ],
203
- "edges": [
204
- {
205
- "id": "cd19ba68-3f39-4b50-8014-85f01258fbe3",
206
- "type": "edge",
207
- "sourceId": "7e51efb8-7463-4775-ad69-180442a34444",
208
- "targetId": "abf8c265-da42-4b81-b7bf-349d3e248294",
209
- },
210
- {
211
- "id": "7c26024c-0f02-4393-8cf0-0f5097cd21d0",
212
- "type": "edge",
213
- "sourceId": "abf8c265-da42-4b81-b7bf-349d3e248294",
214
- "targetId": "40abc689-6e0e-40ee-bc76-df51065e6ff5",
215
- },
216
- {
217
- "id": "66d1a27e-2cf2-4b2e-862d-b65dc554c320",
218
- "type": "edge",
219
- "sourceId": "abf8c265-da42-4b81-b7bf-349d3e248294",
220
- "targetId": "a5385f35-5ba7-4cbf-a5b8-f87bca6fd95c",
221
- },
222
- {
223
- "id": "abf1329e-c27d-4655-b1a7-d410ea03b998",
224
- "type": "edge",
225
- "sourceId": "a5385f35-5ba7-4cbf-a5b8-f87bca6fd95c",
226
- "targetId": "40abc689-6e0e-40ee-bc76-df51065e6ff5",
227
- },
228
- {
229
- "id": "d56e19e6-2303-4272-b7df-49e3df75c62f",
230
- "type": "edge",
231
- "sourceId": "a5385f35-5ba7-4cbf-a5b8-f87bca6fd95c",
232
- "targetId": "0689381e-0650-4ba5-b4ba-1a1800f035ca",
233
- },
234
- ],
235
- }
236
-
237
-
238
- async def test_save_rule_endpoints(client: PlanarTestClient, app: PlanarApp):
239
- response = await client.get("/planar/v1/rules/complex_business_rule")
240
-
241
- assert response.status_code == 200
242
-
243
- data = response.json()
244
-
245
- assert len(data["configs"]) == 1
246
-
247
- # save the rule
248
- response = await client.post(
249
- "/planar/v1/rules/complex_business_rule", json=EXPENSE_RULE_JDM
250
- )
251
-
252
- assert response.status_code == 200, response.text
253
-
254
- data = response.json()
255
-
256
- assert len(data["configs"]) == 2
257
-
258
-
259
- PRICING_RULE_JDM = {
260
- "nodes": [
261
- {
262
- "id": "6cc036d3-3350-449e-9b2c-1569b8f86ffc",
263
- "type": "inputNode",
264
- "name": "Input",
265
- "content": {
266
- "schema": '{"$defs": {"TransactionVolumeRow": {"properties": {"period": {"title": "Period", "type": "string"}, "country": {"title": "Country", "type": "string"}, "currency": {"title": "Currency", "type": "string"}, "completed_count": {"title": "Completed Count", "type": "integer"}, "rejected_count": {"title": "Rejected Count", "type": "integer"}}, "required": ["period", "country", "currency", "completed_count", "rejected_count"], "title": "TransactionVolumeRow", "type": "object"}}, "properties": {"rows": {"items": {"$ref": "#/$defs/TransactionVolumeRow"}, "title": "Rows", "type": "array"}}, "required": ["rows"], "title": "PricingInput", "type": "object"}'
267
- },
268
- "position": {"x": 100, "y": 100},
269
- },
270
- {
271
- "id": "3921e9d3-02e3-4a72-b74d-037c80f97eaa",
272
- "type": "decisionTableNode",
273
- "name": "decisionTable1",
274
- "content": {
275
- "hitPolicy": "first",
276
- "rules": [
277
- {
278
- "_id": "15d4429c-39bc-448d-a7f4-187eaea4493a",
279
- "e5688083-30b9-449e-adaf-bf8ff69eb2ac": '"ARS"',
280
- "42c29309-9aa4-4441-bd1a-b1b57d1b628e": "<= 6000",
281
- "71b9d121-5b37-4b0b-b4c2-d29a868fed35": '"Argentina"',
282
- "662e29e1-d0b8-4cf4-a443-3e92f2157054": "100",
283
- "_description": "",
284
- },
285
- {
286
- "_id": "9ef47884-004d-4314-a61c-38778ed7b7d7",
287
- "e5688083-30b9-449e-adaf-bf8ff69eb2ac": "",
288
- "42c29309-9aa4-4441-bd1a-b1b57d1b628e": "",
289
- "71b9d121-5b37-4b0b-b4c2-d29a868fed35": "",
290
- "662e29e1-d0b8-4cf4-a443-3e92f2157054": "1.00",
291
- },
292
- ],
293
- "inputs": [
294
- {
295
- "id": "e5688083-30b9-449e-adaf-bf8ff69eb2ac",
296
- "name": "Currency",
297
- "field": "currency",
298
- },
299
- {
300
- "id": "42c29309-9aa4-4441-bd1a-b1b57d1b628e",
301
- "name": "Completed Count",
302
- "field": "completed_count",
303
- },
304
- {
305
- "id": "71b9d121-5b37-4b0b-b4c2-d29a868fed35",
306
- "name": "Country",
307
- "field": "country",
308
- },
309
- ],
310
- "outputs": [
311
- {
312
- "id": "662e29e1-d0b8-4cf4-a443-3e92f2157054",
313
- "field": "completed_price_per_transaction_usd",
314
- "name": "Completed Price Per Transaction (USD)",
315
- }
316
- ],
317
- "passThrough": True,
318
- "passThorough": None,
319
- "inputField": "rows",
320
- "outputPath": "line_items",
321
- "executionMode": "loop",
322
- },
323
- "position": {"x": 350, "y": 95},
324
- },
325
- {
326
- "id": "a9a82683-5dbb-4eed-8326-83dea36c1d53",
327
- "type": "outputNode",
328
- "name": "Output",
329
- "content": {
330
- "schema": '{"$defs": {"TransactionPricingLine": {"properties": {"period": {"title": "Period", "type": "string"}, "country": {"title": "Country", "type": "string"}, "currency": {"title": "Currency", "type": "string"}, "completed_count": {"title": "Completed Count", "type": "integer"}, "rejected_count": {"title": "Rejected Count", "type": "integer"}, "completed_price_per_transaction_usd": {"title": "Completed Price Per Transaction Usd", "type": "number"}, "rejected_price_per_transaction_usd": {"title": "Rejected Price Per Transaction Usd", "type": "number"}}, "required": ["period", "country", "currency", "completed_count", "rejected_count", "completed_price_per_transaction_usd", "rejected_price_per_transaction_usd"], "title": "TransactionPricingLine", "type": "object"}}, "properties": {"line_items": {"items": {"$ref": "#/$defs/TransactionPricingLine"}, "title": "Line Items", "type": "array"}}, "required": ["line_items"], "title": "PricingRuleOutput", "type": "object"}'
331
- },
332
- "position": {"x": 1195, "y": 60},
333
- },
334
- {
335
- "id": "b384c91d-dbc3-4043-a0f0-a3adef9ac340",
336
- "type": "expressionNode",
337
- "name": "expression1",
338
- "content": {
339
- "expressions": [
340
- {
341
- "id": "b0e3f514-c109-43e4-91ad-3007110d0a35",
342
- "key": "rejected_price_per_transaction_usd",
343
- "value": "200",
344
- }
345
- ],
346
- "passThrough": True,
347
- "inputField": "line_items",
348
- "outputPath": "line_items",
349
- "executionMode": "loop",
350
- },
351
- "position": {"x": 670, "y": 100},
352
- },
353
- {
354
- "id": "b89b13b8-526f-4db2-a524-faeeca0e78d7",
355
- "type": "expressionNode",
356
- "name": "expression2",
357
- "content": {
358
- "expressions": [
359
- {
360
- "id": "b6a9570b-7ea1-4ebb-b5eb-b693fe14ca47",
361
- "key": "line_items",
362
- "value": "line_items",
363
- }
364
- ],
365
- "passThrough": False,
366
- "inputField": None,
367
- "outputPath": None,
368
- "executionMode": "single",
369
- },
370
- "position": {"x": 950, "y": 100},
371
- },
372
- ],
373
- "edges": [
374
- {
375
- "id": "a9c02fbb-3ad6-4f65-a718-16c0e02d7551",
376
- "type": "edge",
377
- "sourceId": "6cc036d3-3350-449e-9b2c-1569b8f86ffc",
378
- "targetId": "3921e9d3-02e3-4a72-b74d-037c80f97eaa",
379
- },
380
- {
381
- "id": "c2959035-5f0d-4317-8ccf-2f885450b669",
382
- "type": "edge",
383
- "sourceId": "3921e9d3-02e3-4a72-b74d-037c80f97eaa",
384
- "targetId": "b384c91d-dbc3-4043-a0f0-a3adef9ac340",
385
- },
386
- {
387
- "id": "6513b9bf-7016-4776-bf84-81418caf7b74",
388
- "type": "edge",
389
- "sourceId": "b384c91d-dbc3-4043-a0f0-a3adef9ac340",
390
- "targetId": "b89b13b8-526f-4db2-a524-faeeca0e78d7",
391
- },
392
- {
393
- "id": "b75e295e-8fb4-45b6-9040-06fdd8d6723e",
394
- "type": "edge",
395
- "sourceId": "b89b13b8-526f-4db2-a524-faeeca0e78d7",
396
- "targetId": "a9a82683-5dbb-4eed-8326-83dea36c1d53",
397
- },
398
- ],
399
- }
400
-
401
-
402
- async def test_save_rule_endpoints_with_jdm(client: PlanarTestClient, app: PlanarApp):
403
- response = await client.get("/planar/v1/rules/pricing_rule")
404
-
405
- assert response.status_code == 200
406
-
407
- data = response.json()
408
-
409
- assert len(data["configs"]) == 1
410
-
411
- # save the rule
412
- response = await client.post("/planar/v1/rules/pricing_rule", json=PRICING_RULE_JDM)
413
-
414
- assert response.status_code == 200
415
-
416
- data = response.json()
417
-
418
- assert len(data["configs"]) == 2
419
-
420
-
421
- async def test_save_rule_endpoints_with_jdm_wrong_type(
422
- client: PlanarTestClient, app: PlanarApp
423
- ):
424
- response = await client.get("/planar/v1/rules/pricing_rule_with_wrong_type")
425
-
426
- assert response.status_code == 200
427
-
428
- data = response.json()
429
-
430
- assert len(data["configs"]) == 1
431
-
432
- # save the rule
433
- response = await client.post(
434
- "/planar/v1/rules/pricing_rule_with_wrong_type",
435
- json=PRICING_RULE_JDM,
436
- )
437
-
438
- assert response.status_code == 400
439
- response_json = response.json()
440
- assert response_json["detail"]["error"] == "ValidationError"
441
- assert response_json["detail"]["object_name"] == "pricing_rule_with_wrong_type"
442
- assert response_json["detail"]["object_type"] == "rule"
443
- assert response_json["detail"]["diagnostics"]["is_valid"] is False
444
- assert (
445
- response_json["detail"]["diagnostics"]["suggested_fix"]["jdm"]["nodes"][0][
446
- "content"
447
- ]["schema"]
448
- == PRICING_RULE_JDM["nodes"][0]["content"]["schema"]
449
- )
450
- # check contains some_other_field
451
- assert (
452
- "some_other_field"
453
- in response_json["detail"]["diagnostics"]["suggested_fix"]["jdm"]["nodes"][2][
454
- "content"
455
- ]["schema"]
456
- )
457
-
458
- assert response_json["detail"]["diagnostics"]["issues"] == [
459
- {
460
- "error_code": "MISSING_FIELD",
461
- "field_path": "some_other_field",
462
- "message": "Field 'some_other_field' is missing in current node",
463
- "reference_value": {
464
- "title": "Some Other Field",
465
- "type": "string",
466
- },
467
- "current_value": None,
468
- "for_object": "outputNode",
469
- }
470
- ]