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/cli.py
ADDED
@@ -0,0 +1,282 @@
|
|
1
|
+
import os
|
2
|
+
import subprocess
|
3
|
+
import sys
|
4
|
+
from pathlib import Path
|
5
|
+
|
6
|
+
import typer
|
7
|
+
import uvicorn
|
8
|
+
from jinja2 import Environment as JinjaEnvironment
|
9
|
+
from jinja2 import FileSystemLoader
|
10
|
+
|
11
|
+
from planar.config import Environment
|
12
|
+
|
13
|
+
app = typer.Typer(help="Planar CLI tool")
|
14
|
+
|
15
|
+
|
16
|
+
class PlanarServer(uvicorn.Server):
|
17
|
+
"""Intercept SIGINT/SIGTERM to trigger early shutdown on the app."""
|
18
|
+
|
19
|
+
def __init__(self, config: uvicorn.Config, app_import_string: str):
|
20
|
+
super().__init__(config)
|
21
|
+
self.app_import_string = app_import_string
|
22
|
+
|
23
|
+
def handle_exit(self, sig, frame):
|
24
|
+
# Import the PlanarApp instance and fire its early-shutdown hook
|
25
|
+
import asyncio
|
26
|
+
import importlib
|
27
|
+
|
28
|
+
module_name, var_name = self.app_import_string.split(":")
|
29
|
+
app_module = importlib.import_module(module_name)
|
30
|
+
planar_app = getattr(app_module, var_name, None)
|
31
|
+
if planar_app and hasattr(planar_app, "graceful_shutdown"):
|
32
|
+
asyncio.create_task(planar_app.graceful_shutdown())
|
33
|
+
|
34
|
+
# Continue with Uvicorn's normal shutdown procedure
|
35
|
+
super().handle_exit(sig, frame)
|
36
|
+
|
37
|
+
|
38
|
+
def find_default_app_path() -> Path:
|
39
|
+
"""Checks for default app file paths (app.py, then main.py)."""
|
40
|
+
for filename in ["app.py", "main.py"]:
|
41
|
+
path = Path(filename)
|
42
|
+
if path.is_file():
|
43
|
+
typer.echo(f"Found default app file: {path}")
|
44
|
+
return path
|
45
|
+
typer.echo(
|
46
|
+
"Error: Could not find app.py or main.py. Please specify the app file using --path.",
|
47
|
+
err=True,
|
48
|
+
)
|
49
|
+
raise typer.Exit(code=1)
|
50
|
+
|
51
|
+
|
52
|
+
def get_module_str_from_path(app_path: Path) -> str:
|
53
|
+
"""Converts a file path to a Python module import string."""
|
54
|
+
try:
|
55
|
+
# Ensure path is absolute before making it relative
|
56
|
+
abs_path = app_path.resolve()
|
57
|
+
# Find the part relative to the current working directory
|
58
|
+
rel_path = abs_path.relative_to(Path.cwd())
|
59
|
+
# Remove .py and replace path separators with dots
|
60
|
+
module_part = str(rel_path.with_suffix("")).replace(os.path.sep, ".")
|
61
|
+
# Add cwd to sys.path if not already there, uvicorn might need it
|
62
|
+
if str(Path.cwd()) not in sys.path:
|
63
|
+
sys.path.insert(0, str(Path.cwd()))
|
64
|
+
return module_part
|
65
|
+
except ValueError:
|
66
|
+
typer.echo(
|
67
|
+
f"Error: App path {app_path} is not within the current working directory structure.",
|
68
|
+
err=True,
|
69
|
+
)
|
70
|
+
typer.echo("Planar must be run from the project root directory.")
|
71
|
+
raise typer.Exit(code=1)
|
72
|
+
except Exception as e:
|
73
|
+
typer.echo(f"Error processing app path {app_path}: {e}", err=True)
|
74
|
+
raise typer.Exit(code=1)
|
75
|
+
|
76
|
+
|
77
|
+
@app.command("dev")
|
78
|
+
def dev_command(
|
79
|
+
path: Path | None = typer.Argument(
|
80
|
+
None,
|
81
|
+
help="Optional path to the Python file containing the Planar app instance. Defaults to app.py or main.py.",
|
82
|
+
show_default=False, # Hide default in help, as it's dynamic
|
83
|
+
),
|
84
|
+
port: int | None = typer.Option(8000, help="Port to run on"),
|
85
|
+
host: str | None = typer.Option("127.0.0.1", help="Host to run on"),
|
86
|
+
config: Path | None = typer.Option(
|
87
|
+
None, help="Path to config file (default: planar.dev.yaml)"
|
88
|
+
),
|
89
|
+
app_name: str = typer.Option(
|
90
|
+
"app", "--app", help="Name of the PlanarApp instance variable."
|
91
|
+
),
|
92
|
+
script: bool = typer.Option(
|
93
|
+
False,
|
94
|
+
"--script",
|
95
|
+
help="Run as a script with 'uv run' instead of starting a server",
|
96
|
+
),
|
97
|
+
):
|
98
|
+
"""Run Planar in development mode"""
|
99
|
+
run_command(Environment.DEV, port, host, config, path, app_name, script)
|
100
|
+
|
101
|
+
|
102
|
+
@app.command("prod")
|
103
|
+
def prod_command(
|
104
|
+
path: Path | None = typer.Argument(
|
105
|
+
None,
|
106
|
+
help="Optional path to the Python file containing the Planar app instance. Defaults to app.py or main.py.",
|
107
|
+
show_default=False, # Hide default in help, as it's dynamic
|
108
|
+
),
|
109
|
+
port: int | None = typer.Option(8000, help="Port to run on"),
|
110
|
+
host: str | None = typer.Option("0.0.0.0", help="Host to run on"),
|
111
|
+
config: Path | None = typer.Option(
|
112
|
+
None, help="Path to config file (default: planar.prod.yaml)"
|
113
|
+
),
|
114
|
+
app_name: str = typer.Option(
|
115
|
+
"app", "--app", help="Name of the PlanarApp instance variable."
|
116
|
+
),
|
117
|
+
script: bool = typer.Option(
|
118
|
+
False,
|
119
|
+
"--script",
|
120
|
+
help="Run as a script with 'uv run' instead of starting a server",
|
121
|
+
),
|
122
|
+
):
|
123
|
+
"""Run Planar in production mode"""
|
124
|
+
run_command(Environment.PROD, port, host, config, path, app_name, script)
|
125
|
+
|
126
|
+
|
127
|
+
def run_command(
|
128
|
+
env: Environment,
|
129
|
+
port: int | None,
|
130
|
+
host: str | None,
|
131
|
+
config_file: Path | None,
|
132
|
+
path: Path | None,
|
133
|
+
app_name: str,
|
134
|
+
script: bool = False,
|
135
|
+
):
|
136
|
+
"""Common logic for both dev and prod commands"""
|
137
|
+
os.environ["PLANAR_ENV"] = env.value
|
138
|
+
|
139
|
+
if config_file:
|
140
|
+
if not config_file.exists():
|
141
|
+
typer.echo(f"Error: Config file {config_file} not found", err=True)
|
142
|
+
raise typer.Exit(code=1)
|
143
|
+
os.environ["PLANAR_CONFIG"] = str(config_file)
|
144
|
+
|
145
|
+
# Determine the app path
|
146
|
+
if path: # Use the positional argument if provided
|
147
|
+
app_path = path
|
148
|
+
if not app_path.is_file():
|
149
|
+
typer.echo(
|
150
|
+
f"Error: Specified app path {app_path} not found or is not a file",
|
151
|
+
err=True,
|
152
|
+
)
|
153
|
+
raise typer.Exit(code=1)
|
154
|
+
else:
|
155
|
+
app_path = find_default_app_path() # Finds app.py or main.py
|
156
|
+
|
157
|
+
os.environ["PLANAR_ENTRY_POINT"] = str(app_path)
|
158
|
+
|
159
|
+
if script:
|
160
|
+
# Run as a script using uv run
|
161
|
+
typer.echo(f"Running script: {app_path}")
|
162
|
+
typer.echo(f"Environment: {env.value}")
|
163
|
+
|
164
|
+
try:
|
165
|
+
result = subprocess.run(
|
166
|
+
["uv", "run", str(app_path)], env=os.environ.copy(), check=True
|
167
|
+
)
|
168
|
+
raise typer.Exit(code=result.returncode)
|
169
|
+
except subprocess.CalledProcessError as e:
|
170
|
+
typer.echo(f"Error running script: {e}", err=True)
|
171
|
+
raise typer.Exit(code=e.returncode)
|
172
|
+
except FileNotFoundError:
|
173
|
+
typer.echo(
|
174
|
+
"Error: 'uv' command not found. Please install uv first.", err=True
|
175
|
+
)
|
176
|
+
raise typer.Exit(code=1)
|
177
|
+
except Exception as e:
|
178
|
+
typer.echo(f"Error running script: {e}", err=True)
|
179
|
+
raise typer.Exit(code=1)
|
180
|
+
else:
|
181
|
+
# Run as a server using uvicorn
|
182
|
+
# Convert path to module string
|
183
|
+
module_part = get_module_str_from_path(app_path)
|
184
|
+
# TODO: Check that the app_name is a valid variable name in the module
|
185
|
+
app_import_string = f"{module_part}:{app_name}"
|
186
|
+
|
187
|
+
typer.echo(f"Using app: {app_import_string}")
|
188
|
+
typer.echo(f"Starting Planar in {env.value} mode")
|
189
|
+
|
190
|
+
try:
|
191
|
+
config = uvicorn.Config(
|
192
|
+
app_import_string,
|
193
|
+
host=host or ("127.0.0.1" if env == Environment.DEV else "0.0.0.0"),
|
194
|
+
port=port or 8000,
|
195
|
+
reload=True if env == Environment.DEV else False,
|
196
|
+
timeout_graceful_shutdown=4,
|
197
|
+
)
|
198
|
+
|
199
|
+
PlanarServer(config, app_import_string).run()
|
200
|
+
except Exception as e:
|
201
|
+
# Provide more context on import errors
|
202
|
+
if isinstance(e, (ImportError, AttributeError)):
|
203
|
+
typer.echo(
|
204
|
+
f"Error importing application '{app_import_string}': {e}", err=True
|
205
|
+
)
|
206
|
+
typer.echo(
|
207
|
+
f"Ensure '{app_path}' exists and contains a variable named '{app_name}'.",
|
208
|
+
err=True,
|
209
|
+
)
|
210
|
+
else:
|
211
|
+
typer.echo(f"Error starting the application: {e}", err=True)
|
212
|
+
raise typer.Exit(code=1)
|
213
|
+
|
214
|
+
|
215
|
+
@app.command("scaffold")
|
216
|
+
def scaffold_project(
|
217
|
+
name: str = typer.Option(None, "--name", help="Name of the new project"),
|
218
|
+
directory: Path = typer.Option(Path("."), "--directory", help="Target directory"),
|
219
|
+
):
|
220
|
+
"""
|
221
|
+
Creates a new Planar project with a basic structure and example workflow.
|
222
|
+
"""
|
223
|
+
if not name:
|
224
|
+
name = typer.prompt("Project name", default="planar_demo")
|
225
|
+
|
226
|
+
project_dir = directory / name
|
227
|
+
if project_dir.exists():
|
228
|
+
typer.echo(f"Error: Directory {project_dir} already exists", err=True)
|
229
|
+
raise typer.Exit(code=1)
|
230
|
+
|
231
|
+
# Setup Jinja2 template environment
|
232
|
+
template_dir = Path(__file__).parent / "scaffold_templates"
|
233
|
+
jinja_env = JinjaEnvironment(loader=FileSystemLoader(template_dir))
|
234
|
+
|
235
|
+
# Template context
|
236
|
+
context = {"name": name}
|
237
|
+
|
238
|
+
# Create project structure
|
239
|
+
try:
|
240
|
+
(project_dir / "app" / "db").mkdir(parents=True)
|
241
|
+
(project_dir / "app" / "flows").mkdir(parents=True)
|
242
|
+
except OSError as exc:
|
243
|
+
typer.echo(f"Error creating project structure: {exc}", err=True)
|
244
|
+
raise typer.Exit(code=1)
|
245
|
+
|
246
|
+
# Render and write templates
|
247
|
+
templates = [
|
248
|
+
("app/__init__.py.j2", "app/__init__.py"),
|
249
|
+
("app/db/entities.py.j2", "app/db/entities.py"),
|
250
|
+
("app/flows/process_invoice.py.j2", "app/flows/process_invoice.py"),
|
251
|
+
("main.py.j2", "main.py"),
|
252
|
+
("pyproject.toml.j2", "pyproject.toml"),
|
253
|
+
("planar.dev.yaml.j2", "planar.dev.yaml"),
|
254
|
+
("planar.prod.yaml.j2", "planar.prod.yaml"),
|
255
|
+
]
|
256
|
+
|
257
|
+
for template_path, output_path in templates:
|
258
|
+
template = jinja_env.get_template(template_path)
|
259
|
+
content = template.render(context)
|
260
|
+
(project_dir / output_path).write_text(content)
|
261
|
+
|
262
|
+
typer.secho(
|
263
|
+
f"""
|
264
|
+
✨ Project '{name}' created successfully!
|
265
|
+
|
266
|
+
To get started:
|
267
|
+
cd {name}
|
268
|
+
|
269
|
+
Set your OPENAI_API_KEY in a new .env.dev file
|
270
|
+
|
271
|
+
uv run planar dev
|
272
|
+
""",
|
273
|
+
fg=typer.colors.GREEN,
|
274
|
+
)
|
275
|
+
|
276
|
+
|
277
|
+
def main():
|
278
|
+
app()
|
279
|
+
|
280
|
+
|
281
|
+
if __name__ == "__main__":
|
282
|
+
app()
|