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
planar/test_app.py
DELETED
@@ -1,142 +0,0 @@
|
|
1
|
-
from unittest.mock import Mock, patch
|
2
|
-
|
3
|
-
import pytest
|
4
|
-
from dotenv import load_dotenv
|
5
|
-
from fastapi import APIRouter
|
6
|
-
from pydantic import BaseModel, ValidationError
|
7
|
-
|
8
|
-
from examples.simple_service.models import (
|
9
|
-
Invoice,
|
10
|
-
)
|
11
|
-
from planar import PlanarApp, sqlite_config
|
12
|
-
from planar.app import setup_auth_middleware
|
13
|
-
from planar.config import Environment, JWTConfig, SecurityConfig
|
14
|
-
|
15
|
-
load_dotenv()
|
16
|
-
|
17
|
-
|
18
|
-
router = APIRouter()
|
19
|
-
|
20
|
-
|
21
|
-
class InvoiceRequest(BaseModel):
|
22
|
-
message: str
|
23
|
-
|
24
|
-
|
25
|
-
class InvoiceResponse(BaseModel):
|
26
|
-
status: str
|
27
|
-
echo: str
|
28
|
-
|
29
|
-
|
30
|
-
app = PlanarApp(
|
31
|
-
config=sqlite_config("simple_service.db"),
|
32
|
-
title="Sample Invoice API",
|
33
|
-
description="API for CRUD'ing invoices",
|
34
|
-
)
|
35
|
-
|
36
|
-
|
37
|
-
def test_register_model_deduplication():
|
38
|
-
"""Test that registering the same model multiple times only adds it once to the registry."""
|
39
|
-
|
40
|
-
# Ensure Invoice is registered (ObjectRegistry gets reset before each test)
|
41
|
-
app.register_entity(Invoice)
|
42
|
-
initial_model_count = len(app._object_registry.get_entities())
|
43
|
-
|
44
|
-
# Register the Invoice model again
|
45
|
-
app.register_entity(Invoice)
|
46
|
-
|
47
|
-
assert len(app._object_registry.get_entities()) == initial_model_count
|
48
|
-
|
49
|
-
# Register the same model a second time
|
50
|
-
app.register_entity(Invoice)
|
51
|
-
|
52
|
-
assert len(app._object_registry.get_entities()) == initial_model_count
|
53
|
-
|
54
|
-
# Verify that the model in the registry is the Invoice model
|
55
|
-
registered_models = app._object_registry.get_entities()
|
56
|
-
assert any(model.__name__ == "Invoice" for model in registered_models)
|
57
|
-
|
58
|
-
|
59
|
-
class TestJWTSetup:
|
60
|
-
def test_setup_jwt_middleware_production_requires_config(self):
|
61
|
-
"""Test that JWT setup throws ValueError in production without proper config"""
|
62
|
-
mock_app = Mock()
|
63
|
-
mock_app.config.environment = Environment.PROD
|
64
|
-
mock_app.config.security = SecurityConfig()
|
65
|
-
|
66
|
-
with pytest.raises(
|
67
|
-
ValueError,
|
68
|
-
match="Auth middleware is required in production. Please set the JWT config and optionally service token config.",
|
69
|
-
):
|
70
|
-
setup_auth_middleware(mock_app)
|
71
|
-
|
72
|
-
def test_setup_jwt_middleware_production_requires_client_id(self):
|
73
|
-
"""Test that JWT setup throws ValueError in production without client_id"""
|
74
|
-
with pytest.raises(
|
75
|
-
ValidationError,
|
76
|
-
match="Both client_id and org_id required to enable JWT",
|
77
|
-
):
|
78
|
-
JWTConfig(client_id=None, org_id="test-org")
|
79
|
-
|
80
|
-
def test_setup_jwt_middleware_production_requires_org_id(self):
|
81
|
-
"""Test that JWT setup throws ValueError in production without org_id"""
|
82
|
-
with pytest.raises(
|
83
|
-
ValidationError,
|
84
|
-
match="Both client_id and org_id required to enable JWT",
|
85
|
-
):
|
86
|
-
JWTConfig(client_id="test-client-id", org_id=None)
|
87
|
-
|
88
|
-
@patch("planar.app.logger")
|
89
|
-
def test_setup_jwt_middleware_success_with_all_fields(self, mock_logger):
|
90
|
-
"""Test that JWT setup succeeds with all required fields"""
|
91
|
-
mock_app = Mock()
|
92
|
-
mock_app.config.environment = Environment.PROD
|
93
|
-
mock_app.config.security = SecurityConfig(
|
94
|
-
jwt=JWTConfig(
|
95
|
-
client_id="test-client-id",
|
96
|
-
org_id="test-org-id",
|
97
|
-
additional_exclusion_paths=["/test/path"],
|
98
|
-
)
|
99
|
-
)
|
100
|
-
|
101
|
-
setup_auth_middleware(mock_app)
|
102
|
-
|
103
|
-
# Verify middleware was added to app.fastapi
|
104
|
-
mock_app.fastapi.add_middleware.assert_called_once()
|
105
|
-
|
106
|
-
# Check that info log was called
|
107
|
-
mock_logger.info.assert_called_once_with(
|
108
|
-
"Auth middleware enabled",
|
109
|
-
client_id="test-client-id",
|
110
|
-
org_id="test-org-id",
|
111
|
-
additional_exclusion_paths=["/test/path"],
|
112
|
-
)
|
113
|
-
|
114
|
-
@patch("planar.app.logger")
|
115
|
-
def test_setup_jwt_middleware_dev_environment_allows_missing_config(
|
116
|
-
self, mock_logger
|
117
|
-
):
|
118
|
-
"""Test that JWT setup is skipped in dev environment without config"""
|
119
|
-
mock_app = Mock()
|
120
|
-
mock_app.config.environment = Environment.DEV
|
121
|
-
mock_app.config.security = SecurityConfig()
|
122
|
-
|
123
|
-
setup_auth_middleware(mock_app)
|
124
|
-
|
125
|
-
# Verify warning was logged and no middleware added
|
126
|
-
mock_logger.warning.assert_called_once_with("Auth middleware disabled")
|
127
|
-
mock_app.fastapi.add_middleware.assert_not_called()
|
128
|
-
|
129
|
-
@patch("planar.app.logger")
|
130
|
-
def test_setup_jwt_middleware_dev_environment_allows_disabled_jwt(
|
131
|
-
self, mock_logger
|
132
|
-
):
|
133
|
-
"""Test that JWT setup is skipped in dev environment with disabled JWT"""
|
134
|
-
mock_app = Mock()
|
135
|
-
mock_app.config.environment = Environment.DEV
|
136
|
-
mock_app.config.security = SecurityConfig()
|
137
|
-
|
138
|
-
setup_auth_middleware(mock_app)
|
139
|
-
|
140
|
-
# Verify warning was logged and no middleware added
|
141
|
-
mock_logger.warning.assert_called_once_with("Auth middleware disabled")
|
142
|
-
mock_app.fastapi.add_middleware.assert_not_called()
|
planar/test_cli.py
DELETED
@@ -1,394 +0,0 @@
|
|
1
|
-
from pathlib import Path
|
2
|
-
from unittest.mock import patch
|
3
|
-
|
4
|
-
import pytest
|
5
|
-
import typer
|
6
|
-
from typer.testing import CliRunner
|
7
|
-
|
8
|
-
from planar.cli import app, find_default_app_path, get_module_str_from_path
|
9
|
-
|
10
|
-
|
11
|
-
@pytest.fixture
|
12
|
-
def cli_runner():
|
13
|
-
return CliRunner()
|
14
|
-
|
15
|
-
|
16
|
-
@pytest.fixture
|
17
|
-
def mock_uvicorn_run():
|
18
|
-
with patch("planar.cli.uvicorn.run") as mock_run:
|
19
|
-
yield mock_run
|
20
|
-
|
21
|
-
|
22
|
-
@pytest.fixture
|
23
|
-
def mock_path_exists():
|
24
|
-
with patch("pathlib.Path.exists", return_value=True) as mock_exists:
|
25
|
-
yield mock_exists
|
26
|
-
|
27
|
-
|
28
|
-
@pytest.fixture
|
29
|
-
def mock_path_is_file():
|
30
|
-
with patch("pathlib.Path.is_file", return_value=True) as mock_is_file:
|
31
|
-
yield mock_is_file
|
32
|
-
|
33
|
-
|
34
|
-
class TestDevCommand:
|
35
|
-
def test_dev_command_defaults(
|
36
|
-
self, cli_runner, mock_uvicorn_run, mock_path_is_file
|
37
|
-
):
|
38
|
-
"""Test that 'planar dev' sets correct defaults and invokes uvicorn.run."""
|
39
|
-
env = {}
|
40
|
-
with patch("os.environ", env):
|
41
|
-
result = cli_runner.invoke(app, ["dev"])
|
42
|
-
|
43
|
-
assert result.exit_code == 0
|
44
|
-
mock_uvicorn_run.assert_called_once()
|
45
|
-
|
46
|
-
# Verify uvicorn.run arguments
|
47
|
-
call_args = mock_uvicorn_run.call_args
|
48
|
-
assert call_args[0][0] == "app:app"
|
49
|
-
assert call_args.kwargs["reload"] is True
|
50
|
-
assert call_args.kwargs["host"] == "127.0.0.1"
|
51
|
-
assert call_args.kwargs["port"] == 8000
|
52
|
-
assert call_args.kwargs["timeout_graceful_shutdown"] == 4
|
53
|
-
|
54
|
-
# Verify environment variable
|
55
|
-
assert env["PLANAR_ENV"] == "dev"
|
56
|
-
|
57
|
-
|
58
|
-
class TestProdCommand:
|
59
|
-
def test_prod_command_defaults(
|
60
|
-
self, cli_runner, mock_uvicorn_run, mock_path_is_file
|
61
|
-
):
|
62
|
-
"""Test that 'planar prod' sets correct defaults and invokes uvicorn.run."""
|
63
|
-
env = {}
|
64
|
-
with patch("os.environ", env):
|
65
|
-
result = cli_runner.invoke(app, ["prod"])
|
66
|
-
|
67
|
-
assert result.exit_code == 0
|
68
|
-
mock_uvicorn_run.assert_called_once()
|
69
|
-
|
70
|
-
# Verify uvicorn.run arguments
|
71
|
-
call_args = mock_uvicorn_run.call_args
|
72
|
-
assert call_args[0][0] == "app:app"
|
73
|
-
assert call_args.kwargs["reload"] is False
|
74
|
-
assert call_args.kwargs["host"] == "0.0.0.0"
|
75
|
-
assert call_args.kwargs["port"] == 8000
|
76
|
-
assert call_args.kwargs["timeout_graceful_shutdown"] == 4
|
77
|
-
|
78
|
-
# Verify environment variable
|
79
|
-
assert env["PLANAR_ENV"] == "prod"
|
80
|
-
|
81
|
-
|
82
|
-
class TestArgumentParsing:
|
83
|
-
@pytest.mark.parametrize("command", ["dev", "prod"])
|
84
|
-
def test_custom_port(
|
85
|
-
self, cli_runner, mock_uvicorn_run, mock_path_is_file, command
|
86
|
-
):
|
87
|
-
"""Test custom port settings for both dev and prod commands."""
|
88
|
-
port = "9999"
|
89
|
-
with patch("os.environ", {}):
|
90
|
-
result = cli_runner.invoke(app, [command, "--port", port])
|
91
|
-
|
92
|
-
assert result.exit_code == 0
|
93
|
-
mock_uvicorn_run.assert_called_once()
|
94
|
-
|
95
|
-
# Verify config arguments
|
96
|
-
call_args = mock_uvicorn_run.call_args
|
97
|
-
assert call_args.kwargs["port"] == int(port)
|
98
|
-
|
99
|
-
@pytest.mark.parametrize("command", ["dev", "prod"])
|
100
|
-
def test_custom_host(
|
101
|
-
self, cli_runner, mock_uvicorn_run, mock_path_is_file, command
|
102
|
-
):
|
103
|
-
"""Test custom host settings."""
|
104
|
-
host = "0.0.0.0"
|
105
|
-
with patch("os.environ", {}):
|
106
|
-
result = cli_runner.invoke(app, [command, "--host", host])
|
107
|
-
|
108
|
-
assert result.exit_code == 0
|
109
|
-
mock_uvicorn_run.assert_called_once()
|
110
|
-
|
111
|
-
# Verify config arguments
|
112
|
-
call_args = mock_uvicorn_run.call_args
|
113
|
-
assert call_args.kwargs["host"] == host
|
114
|
-
|
115
|
-
@pytest.mark.parametrize("command", ["dev", "prod"])
|
116
|
-
def test_custom_app_name(
|
117
|
-
self, cli_runner, mock_uvicorn_run, mock_path_is_file, command
|
118
|
-
):
|
119
|
-
"""Test custom app instance name."""
|
120
|
-
with (
|
121
|
-
patch("os.environ", {}),
|
122
|
-
patch("planar.cli.get_module_str_from_path", return_value="app"),
|
123
|
-
):
|
124
|
-
result = cli_runner.invoke(app, [command, "--app", "server"])
|
125
|
-
|
126
|
-
assert result.exit_code == 0
|
127
|
-
mock_uvicorn_run.assert_called_once()
|
128
|
-
|
129
|
-
# Verify config arguments
|
130
|
-
call_args = mock_uvicorn_run.call_args
|
131
|
-
assert call_args[0][0] == "app:server"
|
132
|
-
|
133
|
-
@pytest.mark.parametrize("command", ["dev", "prod"])
|
134
|
-
def test_custom_path(
|
135
|
-
self, cli_runner, mock_uvicorn_run, mock_path_is_file, command
|
136
|
-
):
|
137
|
-
"""Test custom application path."""
|
138
|
-
path = "custom/app.py"
|
139
|
-
with (
|
140
|
-
patch("os.environ", {}),
|
141
|
-
patch("planar.cli.get_module_str_from_path", return_value="custom.app"),
|
142
|
-
):
|
143
|
-
result = cli_runner.invoke(app, [command, path])
|
144
|
-
|
145
|
-
assert result.exit_code == 0
|
146
|
-
mock_uvicorn_run.assert_called_once()
|
147
|
-
|
148
|
-
# Verify config arguments
|
149
|
-
call_args = mock_uvicorn_run.call_args
|
150
|
-
assert call_args[0][0] == "custom.app:app"
|
151
|
-
|
152
|
-
@pytest.mark.parametrize("command", ["dev", "prod"])
|
153
|
-
def test_invalid_path(self, cli_runner, mock_uvicorn_run, command):
|
154
|
-
"""Test handling of invalid path."""
|
155
|
-
with patch("pathlib.Path.is_file", return_value=False):
|
156
|
-
result = cli_runner.invoke(app, [command, "nonexistent/app.py"])
|
157
|
-
|
158
|
-
assert result.exit_code == 1
|
159
|
-
assert "not found" in result.output
|
160
|
-
mock_uvicorn_run.assert_not_called()
|
161
|
-
|
162
|
-
|
163
|
-
class TestAppPathResolution:
|
164
|
-
@pytest.mark.parametrize("command", ["dev", "prod"])
|
165
|
-
def test_explicit_path(
|
166
|
-
self, cli_runner, mock_uvicorn_run, mock_path_is_file, command
|
167
|
-
):
|
168
|
-
"""Test command --path sets PLANAR_ENTRY_POINT and generates correct import string."""
|
169
|
-
env = {}
|
170
|
-
with (
|
171
|
-
patch("os.environ", env),
|
172
|
-
patch("planar.cli.get_module_str_from_path", return_value="path.to.app"),
|
173
|
-
):
|
174
|
-
result = cli_runner.invoke(app, [command, "path/to/app.py"])
|
175
|
-
|
176
|
-
assert result.exit_code == 0
|
177
|
-
assert env["PLANAR_ENTRY_POINT"] == "path/to/app.py"
|
178
|
-
mock_uvicorn_run.assert_called_once()
|
179
|
-
|
180
|
-
# Verify config arguments
|
181
|
-
call_args = mock_uvicorn_run.call_args
|
182
|
-
assert call_args[0][0] == "path.to.app:app"
|
183
|
-
|
184
|
-
@pytest.mark.parametrize(
|
185
|
-
"path,error_text",
|
186
|
-
[
|
187
|
-
("non_existent/app.py", "not found or is not a file"),
|
188
|
-
("directory/", "not found or is not a file"),
|
189
|
-
],
|
190
|
-
)
|
191
|
-
def test_invalid_paths(self, cli_runner, path, error_text):
|
192
|
-
"""Test invalid paths exit with error."""
|
193
|
-
with patch("pathlib.Path.is_file", return_value=False):
|
194
|
-
result = cli_runner.invoke(app, ["dev", path])
|
195
|
-
|
196
|
-
assert result.exit_code == 1
|
197
|
-
assert error_text in result.output
|
198
|
-
|
199
|
-
|
200
|
-
class TestDefaultPathDiscovery:
|
201
|
-
@pytest.mark.parametrize(
|
202
|
-
"file_exists,expected_path",
|
203
|
-
[
|
204
|
-
(lambda p: p.name == "app.py", Path("app.py")),
|
205
|
-
(lambda p: p.name == "main.py", Path("main.py")),
|
206
|
-
],
|
207
|
-
)
|
208
|
-
def test_default_files(
|
209
|
-
self, cli_runner, mock_uvicorn_run, file_exists, expected_path
|
210
|
-
):
|
211
|
-
"""Test default file discovery works for app.py and main.py."""
|
212
|
-
env = {}
|
213
|
-
expected_name = expected_path.stem
|
214
|
-
with (
|
215
|
-
patch("os.environ", env),
|
216
|
-
patch("pathlib.Path.is_file", file_exists),
|
217
|
-
patch("planar.cli.get_module_str_from_path", return_value=expected_name),
|
218
|
-
):
|
219
|
-
result = cli_runner.invoke(app, ["dev"])
|
220
|
-
|
221
|
-
assert result.exit_code == 0
|
222
|
-
assert env["PLANAR_ENTRY_POINT"] == str(expected_path)
|
223
|
-
mock_uvicorn_run.assert_called_once()
|
224
|
-
|
225
|
-
def test_no_default_files(self, cli_runner):
|
226
|
-
"""Test with neither app.py nor main.py existing: Verify exit code 1 and error message."""
|
227
|
-
with patch("pathlib.Path.is_file", return_value=False):
|
228
|
-
result = cli_runner.invoke(app, ["dev"])
|
229
|
-
|
230
|
-
assert result.exit_code == 1
|
231
|
-
assert "Could not find app.py or main.py" in result.output
|
232
|
-
|
233
|
-
|
234
|
-
class TestModuleStringConversion:
|
235
|
-
@pytest.mark.parametrize(
|
236
|
-
"path,expected",
|
237
|
-
[
|
238
|
-
(Path("app.py"), "app"),
|
239
|
-
(Path("src/api/main.py"), "src.api.main"),
|
240
|
-
],
|
241
|
-
)
|
242
|
-
def test_valid_paths(self, path, expected):
|
243
|
-
"""Test module string conversion for valid paths."""
|
244
|
-
abs_path = Path.cwd() / path
|
245
|
-
with (
|
246
|
-
patch("pathlib.Path.resolve", return_value=abs_path),
|
247
|
-
patch("pathlib.Path.relative_to", return_value=path),
|
248
|
-
):
|
249
|
-
result = get_module_str_from_path(path)
|
250
|
-
|
251
|
-
assert result == expected
|
252
|
-
|
253
|
-
def test_outside_cwd(self):
|
254
|
-
"""Test conversion for a path outside CWD results in exit code 1 and error."""
|
255
|
-
path = Path("/tmp/app.py")
|
256
|
-
with (
|
257
|
-
patch("pathlib.Path.resolve", return_value=Path("/tmp/app.py")),
|
258
|
-
patch("pathlib.Path.relative_to", side_effect=ValueError("Not relative")),
|
259
|
-
):
|
260
|
-
with pytest.raises(typer.Exit) as exc_info:
|
261
|
-
get_module_str_from_path(path)
|
262
|
-
|
263
|
-
assert exc_info.value.exit_code == 1
|
264
|
-
|
265
|
-
|
266
|
-
class TestConfigFileHandling:
|
267
|
-
def test_config_handling(self, cli_runner, mock_uvicorn_run, mock_path_is_file):
|
268
|
-
"""Test config file handling."""
|
269
|
-
env = {}
|
270
|
-
with patch("os.environ", env):
|
271
|
-
with patch("pathlib.Path.exists", return_value=True):
|
272
|
-
result = cli_runner.invoke(
|
273
|
-
app, ["dev", "--config", "valid/config.yaml"]
|
274
|
-
)
|
275
|
-
|
276
|
-
assert result.exit_code == 0
|
277
|
-
assert env["PLANAR_CONFIG"] == "valid/config.yaml"
|
278
|
-
mock_uvicorn_run.assert_called_once()
|
279
|
-
|
280
|
-
# Test invalid config
|
281
|
-
with patch("pathlib.Path.exists", return_value=False):
|
282
|
-
result = cli_runner.invoke(app, ["dev", "--config", "invalid/config.yaml"])
|
283
|
-
|
284
|
-
assert result.exit_code == 1
|
285
|
-
assert "Config file invalid/config.yaml not found" in result.output
|
286
|
-
|
287
|
-
|
288
|
-
class TestUvicornInteraction:
|
289
|
-
def test_successful_run(self, cli_runner, mock_uvicorn_run, mock_path_is_file):
|
290
|
-
"""Test successful uvicorn.run with correct parameters."""
|
291
|
-
with (
|
292
|
-
patch("os.environ", {}),
|
293
|
-
patch("planar.cli.get_module_str_from_path", return_value="src.main"),
|
294
|
-
):
|
295
|
-
result = cli_runner.invoke(app, ["dev", "src/main.py"])
|
296
|
-
|
297
|
-
assert result.exit_code == 0
|
298
|
-
mock_uvicorn_run.assert_called_once()
|
299
|
-
|
300
|
-
# Verify config arguments
|
301
|
-
call_args = mock_uvicorn_run.call_args
|
302
|
-
assert call_args[0][0] == "src.main:app"
|
303
|
-
assert call_args.kwargs["host"] == "127.0.0.1"
|
304
|
-
assert call_args.kwargs["port"] == 8000
|
305
|
-
assert call_args.kwargs["reload"] is True
|
306
|
-
|
307
|
-
|
308
|
-
class TestScaffoldCommand:
|
309
|
-
def test_scaffold_creates_project_structure(self, cli_runner, tmp_path):
|
310
|
-
"""Test that scaffold command creates the correct directory structure and files."""
|
311
|
-
project_name = "test_project"
|
312
|
-
|
313
|
-
result = cli_runner.invoke(
|
314
|
-
app, ["scaffold", "--name", project_name, "--directory", str(tmp_path)]
|
315
|
-
)
|
316
|
-
|
317
|
-
assert result.exit_code == 0
|
318
|
-
assert "created successfully" in result.output
|
319
|
-
|
320
|
-
# Check project directory exists
|
321
|
-
project_dir = tmp_path / project_name
|
322
|
-
assert project_dir.exists()
|
323
|
-
assert project_dir.is_dir()
|
324
|
-
|
325
|
-
# Check directory structure
|
326
|
-
assert (project_dir / "app").exists()
|
327
|
-
assert (project_dir / "app" / "db").exists()
|
328
|
-
assert (project_dir / "app" / "flows").exists()
|
329
|
-
|
330
|
-
# Check expected files exist
|
331
|
-
expected_files = [
|
332
|
-
"app/__init__.py",
|
333
|
-
"app/db/entities.py",
|
334
|
-
"app/flows/process_invoice.py",
|
335
|
-
"main.py",
|
336
|
-
"pyproject.toml",
|
337
|
-
"planar.dev.yaml",
|
338
|
-
"planar.prod.yaml",
|
339
|
-
]
|
340
|
-
|
341
|
-
for file_path in expected_files:
|
342
|
-
assert (project_dir / file_path).exists(), f"File {file_path} should exist"
|
343
|
-
assert (project_dir / file_path).is_file(), f"{file_path} should be a file"
|
344
|
-
|
345
|
-
def test_scaffold_fails_if_directory_exists(self, cli_runner, tmp_path):
|
346
|
-
"""Test that scaffold command fails if target directory already exists."""
|
347
|
-
project_name = "existing_project"
|
348
|
-
existing_dir = tmp_path / project_name
|
349
|
-
existing_dir.mkdir()
|
350
|
-
|
351
|
-
result = cli_runner.invoke(
|
352
|
-
app, ["scaffold", "--name", project_name, "--directory", str(tmp_path)]
|
353
|
-
)
|
354
|
-
|
355
|
-
assert result.exit_code == 1
|
356
|
-
assert "already exists" in result.output
|
357
|
-
|
358
|
-
def test_scaffold_with_default_directory(self, cli_runner, tmp_path, monkeypatch):
|
359
|
-
"""Test scaffold command with default directory (current directory)."""
|
360
|
-
project_name = "test_project"
|
361
|
-
|
362
|
-
# Change to tmp_path to test default directory behavior
|
363
|
-
monkeypatch.chdir(tmp_path)
|
364
|
-
|
365
|
-
result = cli_runner.invoke(app, ["scaffold", "--name", project_name])
|
366
|
-
|
367
|
-
assert result.exit_code == 0
|
368
|
-
|
369
|
-
# Check project directory exists in current directory
|
370
|
-
project_dir = tmp_path / project_name
|
371
|
-
assert project_dir.exists()
|
372
|
-
assert (project_dir / "main.py").exists()
|
373
|
-
|
374
|
-
|
375
|
-
class TestUtilityFunctions:
|
376
|
-
@pytest.mark.parametrize(
|
377
|
-
"file_exists,expected",
|
378
|
-
[
|
379
|
-
(lambda p: p.name == "app.py", Path("app.py")),
|
380
|
-
(lambda p: p.name == "main.py", Path("main.py")),
|
381
|
-
],
|
382
|
-
)
|
383
|
-
def test_find_default_app_path(self, file_exists, expected):
|
384
|
-
"""Test find_default_app_path function."""
|
385
|
-
with patch("pathlib.Path.is_file", file_exists):
|
386
|
-
path = find_default_app_path()
|
387
|
-
assert path == expected
|
388
|
-
|
389
|
-
def test_find_default_app_path_no_files(self):
|
390
|
-
"""Test find_default_app_path raises typer.Exit when no default files exist."""
|
391
|
-
with patch("pathlib.Path.is_file", return_value=False):
|
392
|
-
with pytest.raises(typer.Exit) as exc_info:
|
393
|
-
find_default_app_path()
|
394
|
-
assert exc_info.value.exit_code == 1
|