planar 0.5.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/.__init__.py.un~ +0 -0
- planar/._version.py.un~ +0 -0
- planar/.app.py.un~ +0 -0
- planar/.cli.py.un~ +0 -0
- planar/.config.py.un~ +0 -0
- planar/.context.py.un~ +0 -0
- planar/.db.py.un~ +0 -0
- planar/.di.py.un~ +0 -0
- planar/.engine.py.un~ +0 -0
- planar/.files.py.un~ +0 -0
- planar/.log_context.py.un~ +0 -0
- planar/.log_metadata.py.un~ +0 -0
- planar/.logging.py.un~ +0 -0
- planar/.object_registry.py.un~ +0 -0
- planar/.otel.py.un~ +0 -0
- planar/.server.py.un~ +0 -0
- planar/.session.py.un~ +0 -0
- planar/.sqlalchemy.py.un~ +0 -0
- planar/.task_local.py.un~ +0 -0
- planar/.test_app.py.un~ +0 -0
- planar/.test_config.py.un~ +0 -0
- planar/.test_object_config.py.un~ +0 -0
- planar/.test_sqlalchemy.py.un~ +0 -0
- planar/.test_utils.py.un~ +0 -0
- planar/.util.py.un~ +0 -0
- planar/.utils.py.un~ +0 -0
- planar/__init__.py +26 -0
- planar/_version.py +1 -0
- planar/ai/.__init__.py.un~ +0 -0
- planar/ai/._models.py.un~ +0 -0
- planar/ai/.agent.py.un~ +0 -0
- planar/ai/.agent_utils.py.un~ +0 -0
- planar/ai/.events.py.un~ +0 -0
- planar/ai/.files.py.un~ +0 -0
- planar/ai/.models.py.un~ +0 -0
- planar/ai/.providers.py.un~ +0 -0
- planar/ai/.pydantic_ai.py.un~ +0 -0
- planar/ai/.pydantic_ai_agent.py.un~ +0 -0
- planar/ai/.pydantic_ai_provider.py.un~ +0 -0
- planar/ai/.step.py.un~ +0 -0
- planar/ai/.test_agent.py.un~ +0 -0
- planar/ai/.test_agent_serialization.py.un~ +0 -0
- planar/ai/.test_providers.py.un~ +0 -0
- planar/ai/.utils.py.un~ +0 -0
- planar/ai/__init__.py +15 -0
- planar/ai/agent.py +457 -0
- planar/ai/agent_utils.py +205 -0
- planar/ai/models.py +140 -0
- planar/ai/providers.py +1088 -0
- planar/ai/test_agent.py +1298 -0
- planar/ai/test_agent_serialization.py +229 -0
- planar/ai/test_providers.py +463 -0
- planar/ai/utils.py +102 -0
- planar/app.py +494 -0
- planar/cli.py +282 -0
- planar/config.py +544 -0
- planar/db/.db.py.un~ +0 -0
- planar/db/__init__.py +17 -0
- planar/db/alembic/env.py +136 -0
- planar/db/alembic/script.py.mako +28 -0
- planar/db/alembic/versions/3476068c153c_initial_system_tables_migration.py +339 -0
- planar/db/alembic.ini +128 -0
- planar/db/db.py +318 -0
- planar/files/.config.py.un~ +0 -0
- planar/files/.local.py.un~ +0 -0
- planar/files/.local_filesystem.py.un~ +0 -0
- planar/files/.model.py.un~ +0 -0
- planar/files/.models.py.un~ +0 -0
- planar/files/.s3.py.un~ +0 -0
- planar/files/.storage.py.un~ +0 -0
- planar/files/.test_files.py.un~ +0 -0
- planar/files/__init__.py +2 -0
- planar/files/models.py +162 -0
- planar/files/storage/.__init__.py.un~ +0 -0
- planar/files/storage/.base.py.un~ +0 -0
- planar/files/storage/.config.py.un~ +0 -0
- planar/files/storage/.context.py.un~ +0 -0
- planar/files/storage/.local_directory.py.un~ +0 -0
- planar/files/storage/.test_local_directory.py.un~ +0 -0
- planar/files/storage/.test_s3.py.un~ +0 -0
- planar/files/storage/base.py +61 -0
- planar/files/storage/config.py +44 -0
- planar/files/storage/context.py +15 -0
- planar/files/storage/local_directory.py +188 -0
- planar/files/storage/s3.py +220 -0
- planar/files/storage/test_local_directory.py +162 -0
- planar/files/storage/test_s3.py +299 -0
- planar/files/test_files.py +283 -0
- planar/human/.human.py.un~ +0 -0
- planar/human/.test_human.py.un~ +0 -0
- planar/human/__init__.py +2 -0
- planar/human/human.py +458 -0
- planar/human/models.py +80 -0
- planar/human/test_human.py +385 -0
- planar/logging/.__init__.py.un~ +0 -0
- planar/logging/.attributes.py.un~ +0 -0
- planar/logging/.formatter.py.un~ +0 -0
- planar/logging/.logger.py.un~ +0 -0
- planar/logging/.otel.py.un~ +0 -0
- planar/logging/.tracer.py.un~ +0 -0
- planar/logging/__init__.py +10 -0
- planar/logging/attributes.py +54 -0
- planar/logging/context.py +14 -0
- planar/logging/formatter.py +113 -0
- planar/logging/logger.py +114 -0
- planar/logging/otel.py +51 -0
- planar/modeling/.mixin.py.un~ +0 -0
- planar/modeling/.storage.py.un~ +0 -0
- planar/modeling/__init__.py +0 -0
- planar/modeling/field_helpers.py +59 -0
- planar/modeling/json_schema_generator.py +94 -0
- planar/modeling/mixins/__init__.py +10 -0
- planar/modeling/mixins/auditable.py +52 -0
- planar/modeling/mixins/test_auditable.py +97 -0
- planar/modeling/mixins/test_timestamp.py +134 -0
- planar/modeling/mixins/test_uuid_primary_key.py +52 -0
- planar/modeling/mixins/timestamp.py +53 -0
- planar/modeling/mixins/uuid_primary_key.py +19 -0
- planar/modeling/orm/.planar_base_model.py.un~ +0 -0
- planar/modeling/orm/__init__.py +18 -0
- planar/modeling/orm/planar_base_entity.py +29 -0
- planar/modeling/orm/query_filter_builder.py +122 -0
- planar/modeling/orm/reexports.py +15 -0
- planar/object_config/.object_config.py.un~ +0 -0
- planar/object_config/__init__.py +11 -0
- planar/object_config/models.py +114 -0
- planar/object_config/object_config.py +378 -0
- planar/object_registry.py +100 -0
- planar/registry_items.py +65 -0
- planar/routers/.__init__.py.un~ +0 -0
- planar/routers/.agents_router.py.un~ +0 -0
- planar/routers/.crud.py.un~ +0 -0
- planar/routers/.decision.py.un~ +0 -0
- planar/routers/.event.py.un~ +0 -0
- planar/routers/.file_attachment.py.un~ +0 -0
- planar/routers/.files.py.un~ +0 -0
- planar/routers/.files_router.py.un~ +0 -0
- planar/routers/.human.py.un~ +0 -0
- planar/routers/.info.py.un~ +0 -0
- planar/routers/.models.py.un~ +0 -0
- planar/routers/.object_config_router.py.un~ +0 -0
- planar/routers/.rule.py.un~ +0 -0
- planar/routers/.test_object_config_router.py.un~ +0 -0
- planar/routers/.test_workflow_router.py.un~ +0 -0
- planar/routers/.workflow.py.un~ +0 -0
- planar/routers/__init__.py +13 -0
- planar/routers/agents_router.py +197 -0
- planar/routers/entity_router.py +143 -0
- planar/routers/event.py +91 -0
- planar/routers/files.py +142 -0
- planar/routers/human.py +151 -0
- planar/routers/info.py +131 -0
- planar/routers/models.py +170 -0
- planar/routers/object_config_router.py +133 -0
- planar/routers/rule.py +108 -0
- planar/routers/test_agents_router.py +174 -0
- planar/routers/test_object_config_router.py +367 -0
- planar/routers/test_routes_security.py +169 -0
- planar/routers/test_rule_router.py +470 -0
- planar/routers/test_workflow_router.py +274 -0
- planar/routers/workflow.py +468 -0
- planar/rules/.decorator.py.un~ +0 -0
- planar/rules/.runner.py.un~ +0 -0
- planar/rules/.test_rules.py.un~ +0 -0
- planar/rules/__init__.py +23 -0
- planar/rules/decorator.py +184 -0
- planar/rules/models.py +355 -0
- planar/rules/rule_configuration.py +191 -0
- planar/rules/runner.py +64 -0
- planar/rules/test_rules.py +750 -0
- planar/scaffold_templates/app/__init__.py.j2 +0 -0
- planar/scaffold_templates/app/db/entities.py.j2 +11 -0
- planar/scaffold_templates/app/flows/process_invoice.py.j2 +67 -0
- planar/scaffold_templates/main.py.j2 +13 -0
- planar/scaffold_templates/planar.dev.yaml.j2 +34 -0
- planar/scaffold_templates/planar.prod.yaml.j2 +28 -0
- planar/scaffold_templates/pyproject.toml.j2 +10 -0
- planar/security/.jwt_middleware.py.un~ +0 -0
- planar/security/auth_context.py +148 -0
- planar/security/authorization.py +388 -0
- planar/security/default_policies.cedar +77 -0
- planar/security/jwt_middleware.py +116 -0
- planar/security/security_context.py +18 -0
- planar/security/tests/test_authorization_context.py +78 -0
- planar/security/tests/test_cedar_basics.py +41 -0
- planar/security/tests/test_cedar_policies.py +158 -0
- planar/security/tests/test_jwt_principal_context.py +179 -0
- planar/session.py +40 -0
- planar/sse/.constants.py.un~ +0 -0
- planar/sse/.example.html.un~ +0 -0
- planar/sse/.hub.py.un~ +0 -0
- planar/sse/.model.py.un~ +0 -0
- planar/sse/.proxy.py.un~ +0 -0
- planar/sse/constants.py +1 -0
- planar/sse/example.html +126 -0
- planar/sse/hub.py +216 -0
- planar/sse/model.py +8 -0
- planar/sse/proxy.py +257 -0
- planar/task_local.py +37 -0
- planar/test_app.py +51 -0
- planar/test_cli.py +372 -0
- planar/test_config.py +512 -0
- planar/test_object_config.py +527 -0
- planar/test_object_registry.py +14 -0
- planar/test_sqlalchemy.py +158 -0
- planar/test_utils.py +105 -0
- planar/testing/.client.py.un~ +0 -0
- planar/testing/.memory_storage.py.un~ +0 -0
- planar/testing/.planar_test_client.py.un~ +0 -0
- planar/testing/.predictable_tracer.py.un~ +0 -0
- planar/testing/.synchronizable_tracer.py.un~ +0 -0
- planar/testing/.test_memory_storage.py.un~ +0 -0
- planar/testing/.workflow_observer.py.un~ +0 -0
- planar/testing/__init__.py +0 -0
- planar/testing/memory_storage.py +78 -0
- planar/testing/planar_test_client.py +54 -0
- planar/testing/synchronizable_tracer.py +153 -0
- planar/testing/test_memory_storage.py +143 -0
- planar/testing/workflow_observer.py +73 -0
- planar/utils.py +70 -0
- planar/workflows/.__init__.py.un~ +0 -0
- planar/workflows/.builtin_steps.py.un~ +0 -0
- planar/workflows/.concurrency_tracing.py.un~ +0 -0
- planar/workflows/.context.py.un~ +0 -0
- planar/workflows/.contrib.py.un~ +0 -0
- planar/workflows/.decorators.py.un~ +0 -0
- planar/workflows/.durable_test.py.un~ +0 -0
- planar/workflows/.errors.py.un~ +0 -0
- planar/workflows/.events.py.un~ +0 -0
- planar/workflows/.exceptions.py.un~ +0 -0
- planar/workflows/.execution.py.un~ +0 -0
- planar/workflows/.human.py.un~ +0 -0
- planar/workflows/.lock.py.un~ +0 -0
- planar/workflows/.misc.py.un~ +0 -0
- planar/workflows/.model.py.un~ +0 -0
- planar/workflows/.models.py.un~ +0 -0
- planar/workflows/.notifications.py.un~ +0 -0
- planar/workflows/.orchestrator.py.un~ +0 -0
- planar/workflows/.runtime.py.un~ +0 -0
- planar/workflows/.serialization.py.un~ +0 -0
- planar/workflows/.step.py.un~ +0 -0
- planar/workflows/.step_core.py.un~ +0 -0
- planar/workflows/.sub_workflow_runner.py.un~ +0 -0
- planar/workflows/.sub_workflow_scheduler.py.un~ +0 -0
- planar/workflows/.test_concurrency.py.un~ +0 -0
- planar/workflows/.test_concurrency_detection.py.un~ +0 -0
- planar/workflows/.test_human.py.un~ +0 -0
- planar/workflows/.test_lock_timeout.py.un~ +0 -0
- planar/workflows/.test_orchestrator.py.un~ +0 -0
- planar/workflows/.test_race_conditions.py.un~ +0 -0
- planar/workflows/.test_serialization.py.un~ +0 -0
- planar/workflows/.test_suspend_deserialization.py.un~ +0 -0
- planar/workflows/.test_workflow.py.un~ +0 -0
- planar/workflows/.tracing.py.un~ +0 -0
- planar/workflows/.types.py.un~ +0 -0
- planar/workflows/.util.py.un~ +0 -0
- planar/workflows/.utils.py.un~ +0 -0
- planar/workflows/.workflow.py.un~ +0 -0
- planar/workflows/.workflow_wrapper.py.un~ +0 -0
- planar/workflows/.wrappers.py.un~ +0 -0
- planar/workflows/__init__.py +42 -0
- planar/workflows/context.py +44 -0
- planar/workflows/contrib.py +190 -0
- planar/workflows/decorators.py +217 -0
- planar/workflows/events.py +185 -0
- planar/workflows/exceptions.py +34 -0
- planar/workflows/execution.py +198 -0
- planar/workflows/lock.py +229 -0
- planar/workflows/misc.py +5 -0
- planar/workflows/models.py +154 -0
- planar/workflows/notifications.py +96 -0
- planar/workflows/orchestrator.py +383 -0
- planar/workflows/query.py +256 -0
- planar/workflows/serialization.py +409 -0
- planar/workflows/step_core.py +373 -0
- planar/workflows/step_metadata.py +357 -0
- planar/workflows/step_testing_utils.py +86 -0
- planar/workflows/sub_workflow_runner.py +191 -0
- planar/workflows/test_concurrency_detection.py +120 -0
- planar/workflows/test_lock_timeout.py +140 -0
- planar/workflows/test_serialization.py +1195 -0
- planar/workflows/test_suspend_deserialization.py +231 -0
- planar/workflows/test_workflow.py +1967 -0
- planar/workflows/tracing.py +106 -0
- planar/workflows/wrappers.py +41 -0
- planar-0.5.0.dist-info/METADATA +285 -0
- planar-0.5.0.dist-info/RECORD +289 -0
- planar-0.5.0.dist-info/WHEEL +4 -0
- planar-0.5.0.dist-info/entry_points.txt +3 -0
planar/test_cli.py
ADDED
@@ -0,0 +1,372 @@
|
|
1
|
+
from pathlib import Path
|
2
|
+
from unittest.mock import MagicMock, 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_planar_server():
|
18
|
+
with patch("planar.cli.PlanarServer") as mock_server_class:
|
19
|
+
mock_server_instance = MagicMock()
|
20
|
+
mock_server_class.return_value = mock_server_instance
|
21
|
+
yield mock_server_class, mock_server_instance
|
22
|
+
|
23
|
+
|
24
|
+
@pytest.fixture
|
25
|
+
def mock_path_exists():
|
26
|
+
with patch("pathlib.Path.exists", return_value=True) as mock_exists:
|
27
|
+
yield mock_exists
|
28
|
+
|
29
|
+
|
30
|
+
@pytest.fixture
|
31
|
+
def mock_path_is_file():
|
32
|
+
with patch("pathlib.Path.is_file", return_value=True) as mock_is_file:
|
33
|
+
yield mock_is_file
|
34
|
+
|
35
|
+
|
36
|
+
class TestDevCommand:
|
37
|
+
def test_dev_command_defaults(
|
38
|
+
self, cli_runner, mock_planar_server, mock_path_is_file
|
39
|
+
):
|
40
|
+
"""Test that 'planar dev' sets correct defaults and invokes uvicorn.run."""
|
41
|
+
env = {}
|
42
|
+
with patch("os.environ", env):
|
43
|
+
result = cli_runner.invoke(app, ["dev"])
|
44
|
+
|
45
|
+
assert result.exit_code == 0
|
46
|
+
mock_planar_server[1].run.assert_called_once()
|
47
|
+
|
48
|
+
# Verify uvicorn.run arguments
|
49
|
+
run_args = mock_planar_server[0].call_args[0]
|
50
|
+
config = run_args[0]
|
51
|
+
assert config.app == "app:app"
|
52
|
+
assert config.reload is True
|
53
|
+
assert config.host == "127.0.0.1"
|
54
|
+
assert config.port == 8000
|
55
|
+
|
56
|
+
# Verify environment variable
|
57
|
+
assert env["PLANAR_ENV"] == "dev"
|
58
|
+
|
59
|
+
|
60
|
+
class TestProdCommand:
|
61
|
+
def test_prod_command_defaults(
|
62
|
+
self, cli_runner, mock_planar_server, mock_path_is_file
|
63
|
+
):
|
64
|
+
"""Test that 'planar prod' sets correct defaults and invokes uvicorn.run."""
|
65
|
+
env = {}
|
66
|
+
with patch("os.environ", env):
|
67
|
+
result = cli_runner.invoke(app, ["prod"])
|
68
|
+
|
69
|
+
assert result.exit_code == 0
|
70
|
+
mock_planar_server[1].run.assert_called_once()
|
71
|
+
|
72
|
+
# Verify uvicorn.run arguments
|
73
|
+
run_args = mock_planar_server[0].call_args[0]
|
74
|
+
config = run_args[0]
|
75
|
+
assert config.app == "app:app"
|
76
|
+
assert config.reload is False
|
77
|
+
assert config.host == "0.0.0.0"
|
78
|
+
assert config.port == 8000
|
79
|
+
|
80
|
+
# Verify environment variable
|
81
|
+
assert env["PLANAR_ENV"] == "prod"
|
82
|
+
|
83
|
+
|
84
|
+
class TestArgumentParsing:
|
85
|
+
@pytest.mark.parametrize("command", ["dev", "prod"])
|
86
|
+
def test_custom_port(
|
87
|
+
self, cli_runner, mock_planar_server, mock_path_is_file, command
|
88
|
+
):
|
89
|
+
"""Test custom port settings for both dev and prod commands."""
|
90
|
+
port = "9999"
|
91
|
+
with patch("os.environ", {}):
|
92
|
+
result = cli_runner.invoke(app, [command, "--port", port])
|
93
|
+
|
94
|
+
assert result.exit_code == 0
|
95
|
+
mock_planar_server[1].run.assert_called_once()
|
96
|
+
|
97
|
+
# Verify config arguments
|
98
|
+
run_args = mock_planar_server[0].call_args[0]
|
99
|
+
config = run_args[0]
|
100
|
+
assert config.port == int(port)
|
101
|
+
|
102
|
+
@pytest.mark.parametrize("command", ["dev", "prod"])
|
103
|
+
def test_custom_host(
|
104
|
+
self, cli_runner, mock_planar_server, mock_path_is_file, command
|
105
|
+
):
|
106
|
+
"""Test custom host settings for both dev and prod commands."""
|
107
|
+
host = "1.1.1.1"
|
108
|
+
with patch("os.environ", {}):
|
109
|
+
result = cli_runner.invoke(app, [command, "--host", host])
|
110
|
+
|
111
|
+
assert result.exit_code == 0
|
112
|
+
mock_planar_server[1].run.assert_called_once()
|
113
|
+
|
114
|
+
# Verify config arguments
|
115
|
+
run_args = mock_planar_server[0].call_args[0]
|
116
|
+
config = run_args[0]
|
117
|
+
assert config.host == host
|
118
|
+
|
119
|
+
@pytest.mark.parametrize("command", ["dev", "prod"])
|
120
|
+
def test_custom_app_name(
|
121
|
+
self, cli_runner, mock_planar_server, mock_path_is_file, command
|
122
|
+
):
|
123
|
+
"""Test specifying --app custom_app generates the correct import string."""
|
124
|
+
with (
|
125
|
+
patch("os.environ", {}),
|
126
|
+
patch("planar.cli.get_module_str_from_path", return_value="module"),
|
127
|
+
):
|
128
|
+
result = cli_runner.invoke(app, [command, "--app", "custom_app"])
|
129
|
+
|
130
|
+
assert result.exit_code == 0
|
131
|
+
mock_planar_server[1].run.assert_called_once()
|
132
|
+
|
133
|
+
# Verify config arguments
|
134
|
+
run_args = mock_planar_server[0].call_args[0]
|
135
|
+
config = run_args[0]
|
136
|
+
assert config.app == "module:custom_app"
|
137
|
+
|
138
|
+
|
139
|
+
class TestAppPathResolution:
|
140
|
+
@pytest.mark.parametrize("command", ["dev", "prod"])
|
141
|
+
def test_explicit_path(
|
142
|
+
self, cli_runner, mock_planar_server, mock_path_is_file, command
|
143
|
+
):
|
144
|
+
"""Test command --path sets PLANAR_ENTRY_POINT and generates correct import string."""
|
145
|
+
env = {}
|
146
|
+
with (
|
147
|
+
patch("os.environ", env),
|
148
|
+
patch("planar.cli.get_module_str_from_path", return_value="path.to.app"),
|
149
|
+
):
|
150
|
+
result = cli_runner.invoke(app, [command, "path/to/app.py"])
|
151
|
+
|
152
|
+
assert result.exit_code == 0
|
153
|
+
assert env["PLANAR_ENTRY_POINT"] == "path/to/app.py"
|
154
|
+
mock_planar_server[1].run.assert_called_once()
|
155
|
+
|
156
|
+
# Verify config arguments
|
157
|
+
run_args = mock_planar_server[0].call_args[0]
|
158
|
+
config = run_args[0]
|
159
|
+
assert config.app == "path.to.app:app"
|
160
|
+
|
161
|
+
@pytest.mark.parametrize(
|
162
|
+
"path,error_text",
|
163
|
+
[
|
164
|
+
("non_existent/app.py", "not found or is not a file"),
|
165
|
+
("directory/", "not found or is not a file"),
|
166
|
+
],
|
167
|
+
)
|
168
|
+
def test_invalid_paths(self, cli_runner, path, error_text):
|
169
|
+
"""Test invalid paths exit with error."""
|
170
|
+
with patch("pathlib.Path.is_file", return_value=False):
|
171
|
+
result = cli_runner.invoke(app, ["dev", path])
|
172
|
+
|
173
|
+
assert result.exit_code == 1
|
174
|
+
assert error_text in result.output
|
175
|
+
|
176
|
+
|
177
|
+
class TestDefaultPathDiscovery:
|
178
|
+
@pytest.mark.parametrize(
|
179
|
+
"file_exists,expected_path",
|
180
|
+
[
|
181
|
+
(lambda p: p.name == "app.py", Path("app.py")),
|
182
|
+
(lambda p: p.name == "main.py", Path("main.py")),
|
183
|
+
],
|
184
|
+
)
|
185
|
+
def test_default_files(
|
186
|
+
self, cli_runner, mock_planar_server, file_exists, expected_path
|
187
|
+
):
|
188
|
+
"""Test default file discovery works for app.py and main.py."""
|
189
|
+
env = {}
|
190
|
+
expected_name = expected_path.stem
|
191
|
+
with (
|
192
|
+
patch("os.environ", env),
|
193
|
+
patch("pathlib.Path.is_file", file_exists),
|
194
|
+
patch("planar.cli.get_module_str_from_path", return_value=expected_name),
|
195
|
+
):
|
196
|
+
result = cli_runner.invoke(app, ["dev"])
|
197
|
+
|
198
|
+
assert result.exit_code == 0
|
199
|
+
assert env["PLANAR_ENTRY_POINT"] == str(expected_path)
|
200
|
+
mock_planar_server[1].run.assert_called_once()
|
201
|
+
|
202
|
+
def test_no_default_files(self, cli_runner):
|
203
|
+
"""Test with neither app.py nor main.py existing: Verify exit code 1 and error message."""
|
204
|
+
with patch("pathlib.Path.is_file", return_value=False):
|
205
|
+
result = cli_runner.invoke(app, ["dev"])
|
206
|
+
|
207
|
+
assert result.exit_code == 1
|
208
|
+
assert "Could not find app.py or main.py" in result.output
|
209
|
+
|
210
|
+
|
211
|
+
class TestModuleStringConversion:
|
212
|
+
@pytest.mark.parametrize(
|
213
|
+
"path,expected",
|
214
|
+
[
|
215
|
+
(Path("app.py"), "app"),
|
216
|
+
(Path("src/api/main.py"), "src.api.main"),
|
217
|
+
],
|
218
|
+
)
|
219
|
+
def test_valid_paths(self, path, expected):
|
220
|
+
"""Test module string conversion for valid paths."""
|
221
|
+
abs_path = Path.cwd() / path
|
222
|
+
with (
|
223
|
+
patch("pathlib.Path.resolve", return_value=abs_path),
|
224
|
+
patch("pathlib.Path.relative_to", return_value=path),
|
225
|
+
):
|
226
|
+
result = get_module_str_from_path(path)
|
227
|
+
|
228
|
+
assert result == expected
|
229
|
+
|
230
|
+
def test_outside_cwd(self):
|
231
|
+
"""Test conversion for a path outside CWD results in exit code 1 and error."""
|
232
|
+
path = Path("/tmp/app.py")
|
233
|
+
with (
|
234
|
+
patch("pathlib.Path.resolve", return_value=Path("/tmp/app.py")),
|
235
|
+
patch("pathlib.Path.relative_to", side_effect=ValueError("Not relative")),
|
236
|
+
):
|
237
|
+
with pytest.raises(typer.Exit) as exc_info:
|
238
|
+
get_module_str_from_path(path)
|
239
|
+
|
240
|
+
assert exc_info.value.exit_code == 1
|
241
|
+
|
242
|
+
|
243
|
+
class TestConfigFileHandling:
|
244
|
+
def test_config_handling(self, cli_runner, mock_planar_server, mock_path_is_file):
|
245
|
+
"""Test config file handling."""
|
246
|
+
env = {}
|
247
|
+
with patch("os.environ", env):
|
248
|
+
with patch("pathlib.Path.exists", return_value=True):
|
249
|
+
result = cli_runner.invoke(
|
250
|
+
app, ["dev", "--config", "valid/config.yaml"]
|
251
|
+
)
|
252
|
+
|
253
|
+
assert result.exit_code == 0
|
254
|
+
assert env["PLANAR_CONFIG"] == "valid/config.yaml"
|
255
|
+
mock_planar_server[1].run.assert_called_once()
|
256
|
+
|
257
|
+
# Test invalid config
|
258
|
+
with patch("pathlib.Path.exists", return_value=False):
|
259
|
+
result = cli_runner.invoke(app, ["dev", "--config", "invalid/config.yaml"])
|
260
|
+
|
261
|
+
assert result.exit_code == 1
|
262
|
+
assert "Config file invalid/config.yaml not found" in result.output
|
263
|
+
|
264
|
+
|
265
|
+
class TestUvicornInteraction:
|
266
|
+
def test_successful_run(self, cli_runner, mock_planar_server, mock_path_is_file):
|
267
|
+
"""Test successful uvicorn.run with correct parameters."""
|
268
|
+
with (
|
269
|
+
patch("os.environ", {}),
|
270
|
+
patch("planar.cli.get_module_str_from_path", return_value="src.main"),
|
271
|
+
):
|
272
|
+
result = cli_runner.invoke(app, ["dev", "src/main.py"])
|
273
|
+
|
274
|
+
assert result.exit_code == 0
|
275
|
+
mock_planar_server[1].run.assert_called_once()
|
276
|
+
|
277
|
+
# Verify config arguments
|
278
|
+
run_args = mock_planar_server[0].call_args[0]
|
279
|
+
config = run_args[0]
|
280
|
+
assert config.app == "src.main:app"
|
281
|
+
assert config.host == "127.0.0.1"
|
282
|
+
assert config.port == 8000
|
283
|
+
assert config.reload is True
|
284
|
+
|
285
|
+
|
286
|
+
class TestScaffoldCommand:
|
287
|
+
def test_scaffold_creates_project_structure(self, cli_runner, tmp_path):
|
288
|
+
"""Test that scaffold command creates the correct directory structure and files."""
|
289
|
+
project_name = "test_project"
|
290
|
+
|
291
|
+
result = cli_runner.invoke(
|
292
|
+
app, ["scaffold", "--name", project_name, "--directory", str(tmp_path)]
|
293
|
+
)
|
294
|
+
|
295
|
+
assert result.exit_code == 0
|
296
|
+
assert "created successfully" in result.output
|
297
|
+
|
298
|
+
# Check project directory exists
|
299
|
+
project_dir = tmp_path / project_name
|
300
|
+
assert project_dir.exists()
|
301
|
+
assert project_dir.is_dir()
|
302
|
+
|
303
|
+
# Check directory structure
|
304
|
+
assert (project_dir / "app").exists()
|
305
|
+
assert (project_dir / "app" / "db").exists()
|
306
|
+
assert (project_dir / "app" / "flows").exists()
|
307
|
+
|
308
|
+
# Check expected files exist
|
309
|
+
expected_files = [
|
310
|
+
"app/__init__.py",
|
311
|
+
"app/db/entities.py",
|
312
|
+
"app/flows/process_invoice.py",
|
313
|
+
"main.py",
|
314
|
+
"pyproject.toml",
|
315
|
+
"planar.dev.yaml",
|
316
|
+
"planar.prod.yaml",
|
317
|
+
]
|
318
|
+
|
319
|
+
for file_path in expected_files:
|
320
|
+
assert (project_dir / file_path).exists(), f"File {file_path} should exist"
|
321
|
+
assert (project_dir / file_path).is_file(), f"{file_path} should be a file"
|
322
|
+
|
323
|
+
def test_scaffold_fails_if_directory_exists(self, cli_runner, tmp_path):
|
324
|
+
"""Test that scaffold command fails if target directory already exists."""
|
325
|
+
project_name = "existing_project"
|
326
|
+
existing_dir = tmp_path / project_name
|
327
|
+
existing_dir.mkdir()
|
328
|
+
|
329
|
+
result = cli_runner.invoke(
|
330
|
+
app, ["scaffold", "--name", project_name, "--directory", str(tmp_path)]
|
331
|
+
)
|
332
|
+
|
333
|
+
assert result.exit_code == 1
|
334
|
+
assert "already exists" in result.output
|
335
|
+
|
336
|
+
def test_scaffold_with_default_directory(self, cli_runner, tmp_path, monkeypatch):
|
337
|
+
"""Test scaffold command with default directory (current directory)."""
|
338
|
+
project_name = "test_project"
|
339
|
+
|
340
|
+
# Change to tmp_path to test default directory behavior
|
341
|
+
monkeypatch.chdir(tmp_path)
|
342
|
+
|
343
|
+
result = cli_runner.invoke(app, ["scaffold", "--name", project_name])
|
344
|
+
|
345
|
+
assert result.exit_code == 0
|
346
|
+
|
347
|
+
# Check project directory exists in current directory
|
348
|
+
project_dir = tmp_path / project_name
|
349
|
+
assert project_dir.exists()
|
350
|
+
assert (project_dir / "main.py").exists()
|
351
|
+
|
352
|
+
|
353
|
+
class TestUtilityFunctions:
|
354
|
+
@pytest.mark.parametrize(
|
355
|
+
"file_exists,expected",
|
356
|
+
[
|
357
|
+
(lambda p: p.name == "app.py", Path("app.py")),
|
358
|
+
(lambda p: p.name == "main.py", Path("main.py")),
|
359
|
+
],
|
360
|
+
)
|
361
|
+
def test_find_default_app_path(self, file_exists, expected):
|
362
|
+
"""Test find_default_app_path function."""
|
363
|
+
with patch("pathlib.Path.is_file", file_exists):
|
364
|
+
path = find_default_app_path()
|
365
|
+
assert path == expected
|
366
|
+
|
367
|
+
def test_find_default_app_path_no_files(self):
|
368
|
+
"""Test find_default_app_path raises typer.Exit when no default files exist."""
|
369
|
+
with patch("pathlib.Path.is_file", return_value=False):
|
370
|
+
with pytest.raises(typer.Exit) as exc_info:
|
371
|
+
find_default_app_path()
|
372
|
+
assert exc_info.value.exit_code == 1
|