planar 0.9.0__py3-none-any.whl → 0.9.2__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/ai/agent_base.py CHANGED
@@ -12,6 +12,7 @@ from typing import (
12
12
  )
13
13
 
14
14
  from pydantic import BaseModel
15
+ from pydantic_ai.settings import ModelSettings
15
16
 
16
17
  from planar.ai.models import AgentConfig, AgentEventEmitter, AgentRunResult
17
18
  from planar.logging import get_logger
@@ -38,7 +39,10 @@ class AgentBase[
38
39
  user_prompt: str = ""
39
40
  tools: list[Callable] = field(default_factory=list)
40
41
  max_turns: int = 2
41
- model_parameters: dict[str, Any] = field(default_factory=dict)
42
+ # `ModelSettings` is a TypedDict; use a typed empty dict as default
43
+ model_parameters: ModelSettings = field(
44
+ default_factory=lambda: cast(ModelSettings, {})
45
+ )
42
46
  event_emitter: AgentEventEmitter | None = None
43
47
  durable: bool = True
44
48
 
planar/ai/agent_utils.py CHANGED
@@ -1,13 +1,10 @@
1
1
  import inspect
2
- from typing import (
3
- Any,
4
- Callable,
5
- Dict,
6
- )
2
+ from typing import Any, Callable, Dict, cast
7
3
 
8
4
  from jinja2 import StrictUndefined, TemplateError
9
5
  from jinja2.sandbox import SandboxedEnvironment
10
- from pydantic import BaseModel, create_model
6
+ from pydantic import BaseModel, Field, create_model
7
+ from pydantic_ai.settings import ModelSettings
11
8
 
12
9
  from planar.ai.models import (
13
10
  AgentConfig,
@@ -24,8 +21,9 @@ logger = get_logger(__name__)
24
21
  class ModelSpec(BaseModel):
25
22
  """Pydantic model for AI model specifications."""
26
23
 
24
+ model_config = {"arbitrary_types_allowed": True}
27
25
  model_id: str
28
- parameters: dict[str, Any] = {}
26
+ parameters: ModelSettings = Field(default_factory=lambda: cast(ModelSettings, {}))
29
27
 
30
28
 
31
29
  def extract_files_from_model(
planar/ai/models.py CHANGED
@@ -1,5 +1,3 @@
1
- from __future__ import annotations
2
-
3
1
  from enum import Enum
4
2
  from typing import (
5
3
  Annotated,
@@ -11,9 +9,11 @@ from typing import (
11
9
  Protocol,
12
10
  TypeVar,
13
11
  Union,
12
+ cast,
14
13
  )
15
14
 
16
15
  from pydantic import BaseModel, Field
16
+ from pydantic_ai.settings import ModelSettings
17
17
 
18
18
  from planar.files.models import PlanarFile
19
19
  from planar.modeling.field_helpers import JsonSchema
@@ -29,11 +29,17 @@ T = TypeVar("T", bound=Union[str, BaseModel])
29
29
  # This model allows storing configurations that override the default
30
30
  # settings defined in Agent instances.
31
31
  class AgentConfig(BaseModel):
32
+ # ModelSettings TypedDict has some fields that use non-serializable types
33
+ # so we need to allow arbitrary types
34
+ model_config = {"arbitrary_types_allowed": True}
32
35
  system_prompt: str
33
36
  user_prompt: str = Field()
34
37
  model: str = Field()
35
38
  max_turns: int = Field()
36
- model_parameters: Dict[str, Any] = Field(default_factory=dict)
39
+ # `ModelSettings` is a TypedDict; use a typed empty dict as default
40
+ model_parameters: ModelSettings = Field(
41
+ default_factory=lambda: cast(ModelSettings, {})
42
+ )
37
43
 
38
44
 
39
45
  class ToolDefinition(BaseModel):
planar/ai/pydantic_ai.py CHANGED
@@ -384,7 +384,7 @@ class ModelRunResponse[TOutput: BaseModel | str](BaseModel):
384
384
  async def model_run[TOutput: BaseModel | str](
385
385
  model: Model | KnownModelName,
386
386
  max_extra_turns: int,
387
- model_settings: dict[str, Any] | None = None,
387
+ model_settings: ModelSettings | None = None,
388
388
  messages: list[m.ModelMessage] = [],
389
389
  tools: list[m.ToolDefinition] = [],
390
390
  event_handler: m.AgentEventEmitter | None = None,
@@ -443,7 +443,7 @@ async def model_run[TOutput: BaseModel | str](
443
443
  model=model,
444
444
  messages=history,
445
445
  model_request_parameters=request_params,
446
- model_settings=cast(ModelSettings, model_settings),
446
+ model_settings=model_settings,
447
447
  ) as stream:
448
448
  async for event in stream:
449
449
  match event:
planar/app.py CHANGED
@@ -80,7 +80,10 @@ class PlanarApp:
80
80
  )
81
81
  self.policy_service: PolicyService | None = None
82
82
 
83
- self.db_manager = DatabaseManager(db_url=self.config.connection_url())
83
+ self.db_manager = DatabaseManager(
84
+ db_url=self.config.connection_url(),
85
+ entity_schema=self.config.app.entity_schema,
86
+ )
84
87
 
85
88
  if self.config.storage:
86
89
  self.storage = create_from_config(self.config.storage)
@@ -220,9 +223,7 @@ class PlanarApp:
220
223
 
221
224
  # Begin the normal lifespan logic
222
225
  self.db_manager.connect()
223
- await self.db_manager.migrate(
224
- self.config.use_alembic if self.config.use_alembic is not None else True
225
- )
226
+ await self.db_manager.migrate()
226
227
 
227
228
  self.orchestrator = WorkflowOrchestrator(self.db_manager.get_engine())
228
229
  config_tok = config_var.set(self.config)
planar/config.py CHANGED
@@ -127,6 +127,10 @@ DatabaseConfig = Annotated[
127
127
  class AppConfig(BaseModel):
128
128
  db_connection: str
129
129
  max_db_conflict_retries: int | None = None
130
+ # Default schema for user-defined entities (PlanarBaseEntity)
131
+ # Postgres: used as the target schema for user tables
132
+ # SQLite: ignored (SQLite has no schemas)
133
+ entity_schema: str = "planar_entity"
130
134
 
131
135
 
132
136
  def default_storage_config() -> StorageConfig:
@@ -224,7 +228,6 @@ class PlanarConfig(BaseModel):
224
228
  environment: Environment = Environment.DEV
225
229
  security: SecurityConfig = SecurityConfig()
226
230
  logging: dict[str, LoggerConfig] | None = None
227
- use_alembic: bool | None = True
228
231
  otel: OtelConfig | None = None
229
232
  data: DataConfig | None = None
230
233
 
planar/data/dataset.py CHANGED
@@ -212,8 +212,21 @@ class PlanarDataset(BaseModel):
212
212
  else:
213
213
  raise ValueError(f"Unsupported catalog type: {catalog_config.type}")
214
214
 
215
+ try:
216
+ await asyncio.to_thread(con.raw_sql, "INSTALL ducklake")
217
+ match catalog_config.type:
218
+ case "sqlite":
219
+ await asyncio.to_thread(con.raw_sql, "INSTALL sqlite;")
220
+ case "postgres":
221
+ await asyncio.to_thread(con.raw_sql, "INSTALL postgres;")
222
+ logger.debug(
223
+ "installed Ducklake extensions", catalog_type=catalog_config.type
224
+ )
225
+ except Exception as e:
226
+ raise DataError(f"Failed to install Ducklake extensions: {e}") from e
227
+
215
228
  # Build ATTACH statement
216
- attach_sql = f"ATTACH 'ducklake:{metadata_path}'"
229
+ attach_sql = f"ATTACH 'ducklake:{metadata_path}' AS planar_ducklake"
217
230
 
218
231
  # Add data path from storage config
219
232
  storage = data_config.storage
@@ -227,7 +240,11 @@ class PlanarDataset(BaseModel):
227
240
  storage, "directory", "."
228
241
  )
229
242
 
230
- attach_sql += f" (DATA_PATH '{data_path}')"
243
+ ducklake_catalog = data_config.catalog_name
244
+ attach_sql += f" (DATA_PATH '{data_path}'"
245
+ if catalog_config.type != "sqlite":
246
+ attach_sql += f", METADATA_SCHEMA '{ducklake_catalog}'"
247
+ attach_sql += ");"
231
248
 
232
249
  # Attach to Ducklake
233
250
  try:
@@ -235,38 +252,12 @@ class PlanarDataset(BaseModel):
235
252
  except Exception as e:
236
253
  raise DataError(f"Failed to attach to Ducklake: {e}") from e
237
254
 
238
- # Check available catalogs (what ibis calls schemas) and use the correct one
239
- try:
240
- catalogs = await asyncio.to_thread(con.list_catalogs)
241
- logger.debug("available catalogs", catalogs=catalogs)
242
-
243
- # Find the ducklake catalog (it might have a different name pattern)
244
- ducklake_catalog = None
245
- for cat in catalogs:
246
- if "ducklake" in cat.lower() or cat == data_config.catalog_name:
247
- ducklake_catalog = cat
248
- break
249
-
250
- if ducklake_catalog:
251
- if not ducklake_catalog.replace("_", "").replace("-", "").isalnum():
252
- raise DataError(f"Invalid catalog name format: {ducklake_catalog}")
253
- await asyncio.to_thread(con.raw_sql, f"USE {ducklake_catalog}")
254
- logger.debug("using catalog", catalog=ducklake_catalog)
255
- else:
256
- catalog_name = data_config.catalog_name
257
- if not catalog_name.replace("_", "").replace("-", "").isalnum():
258
- raise DataError(f"Invalid catalog name format: {catalog_name}")
259
- await asyncio.to_thread(
260
- con.raw_sql,
261
- f"CREATE SCHEMA IF NOT EXISTS {catalog_name}",
262
- )
263
- await asyncio.to_thread(con.raw_sql, f"USE {catalog_name}")
264
- logger.debug(
265
- "created and using catalog", catalog=data_config.catalog_name
266
- )
267
-
268
- except Exception as e:
269
- logger.warning("failed to set catalog, using default", error=str(e))
270
- # Continue without setting catalog - will use qualified names
255
+ await asyncio.to_thread(con.raw_sql, "USE planar_ducklake;")
256
+ logger.debug(
257
+ "connection created",
258
+ catalog=ducklake_catalog,
259
+ catalog_type=catalog_config.type,
260
+ attach_sql=attach_sql,
261
+ )
271
262
 
272
263
  return con
planar/db/alembic/env.py CHANGED
@@ -41,6 +41,13 @@ def run_migrations_offline() -> None:
41
41
  )
42
42
 
43
43
 
44
+ def include_name(name, type_, _):
45
+ if type_ == "schema":
46
+ return name == PLANAR_SCHEMA
47
+ else:
48
+ return True
49
+
50
+
44
51
  def run_migrations_online() -> None:
45
52
  """Run migrations in 'online' mode.
46
53
 
@@ -60,8 +67,8 @@ def run_migrations_online() -> None:
60
67
  target_metadata=target_metadata,
61
68
  # For SQLite, don't use schema since it's not supported
62
69
  version_table_schema=None if is_sqlite else PLANAR_SCHEMA,
63
- include_schemas=not is_sqlite,
64
- compare_type=True,
70
+ include_schemas=True,
71
+ include_name=include_name,
65
72
  # SQLite doesn't support alter table, so we need to use render_as_batch
66
73
  # to create the tables in a single transaction. For other databases,
67
74
  # the batch op is no-op.
@@ -95,7 +102,7 @@ def run_migrations_online() -> None:
95
102
  config_dict = config.get_section(config.config_ini_section, {})
96
103
  url = config_dict["sqlalchemy.url"]
97
104
  is_sqlite = url.startswith("sqlite://")
98
- translate_map = {"planar": None} if is_sqlite else {}
105
+ translate_map = {PLANAR_SCHEMA: None} if is_sqlite else {}
99
106
  connectable = engine_from_config(
100
107
  config_dict,
101
108
  prefix="sqlalchemy.",
@@ -110,15 +117,15 @@ def run_migrations_online() -> None:
110
117
  with connectable.connect() as connection:
111
118
  is_sqlite = connection.dialect.name == "sqlite"
112
119
  if is_sqlite:
113
- connection.dialect.default_schema_name = "planar"
120
+ connection.dialect.default_schema_name = PLANAR_SCHEMA
114
121
 
115
122
  context.configure(
116
123
  connection=connection,
117
124
  target_metadata=target_metadata,
118
125
  # For SQLite, don't use schema since it's not supported
119
126
  version_table_schema=None if is_sqlite else PLANAR_SCHEMA,
120
- include_schemas=not is_sqlite,
121
- compare_type=True,
127
+ include_schemas=True,
128
+ include_name=include_name,
122
129
  # SQLite doesn't support alter table, so we need to use render_as_batch
123
130
  # to create the tables in a single transaction. For other databases,
124
131
  # the batch op is no-op.
@@ -5,12 +5,12 @@ Revises: ${down_revision | comma,n}
5
5
  Create Date: ${create_date}
6
6
 
7
7
  """
8
+
8
9
  from typing import Sequence, Union
9
10
 
10
- from alembic import op
11
11
  import sqlalchemy as sa
12
12
  import sqlmodel.sql.sqltypes
13
- import planar.object_config.models
13
+ from alembic import op
14
14
  ${imports if imports else ""}
15
15
 
16
16
  # revision identifiers, used by Alembic.
planar/db/db.py CHANGED
@@ -20,7 +20,10 @@ from sqlmodel.ext.asyncio.session import AsyncSession
20
20
 
21
21
  import planar
22
22
  from planar.logging import get_logger
23
- from planar.modeling.orm.planar_base_entity import PLANAR_APPLICATION_METADATA
23
+ from planar.modeling.orm.planar_base_entity import (
24
+ PLANAR_APPLICATION_METADATA,
25
+ PLANAR_ENTITY_SCHEMA,
26
+ )
24
27
  from planar.utils import P, R, T, U, exponential_backoff_with_jitter
25
28
 
26
29
 
@@ -170,9 +173,12 @@ class DatabaseManager:
170
173
  def __init__(
171
174
  self,
172
175
  db_url: str | URL,
176
+ *,
177
+ entity_schema: str = PLANAR_ENTITY_SCHEMA,
173
178
  ):
174
179
  self.db_url = make_url(db_url) if isinstance(db_url, str) else db_url
175
180
  self.engine: AsyncEngine | None = None
181
+ self.entity_schema = entity_schema
176
182
 
177
183
  def _create_sqlite_engine(self, url: URL) -> AsyncEngine:
178
184
  # in practice this high timeout is only use
@@ -189,9 +195,14 @@ class DatabaseManager:
189
195
  # even though it is the default value.
190
196
  autocommit=LEGACY_TRANSACTION_CONTROL,
191
197
  ),
192
- # SQLite doesn't support schemas, so we need to translate the planar schema
193
- # name to None.
194
- execution_options={"schema_translate_map": {"planar": None}},
198
+ # SQLite doesn't support schemas, so we need to translate the planar and user
199
+ # schema names to None.
200
+ execution_options={
201
+ "schema_translate_map": {
202
+ "planar": None,
203
+ PLANAR_ENTITY_SCHEMA: None,
204
+ }
205
+ },
195
206
  )
196
207
 
197
208
  def do_begin(conn: Connection):
@@ -202,7 +213,12 @@ class DatabaseManager:
202
213
  return engine
203
214
 
204
215
  def _create_postgresql_engine(self, url: URL) -> AsyncEngine:
205
- engine = create_async_engine(url)
216
+ # Map default (PLANAR_ENTITY_SCHEMA) schema to the configured entity schema for user tables.
217
+ # Leave the system table schema ('planar') unmapped so system tables are not overridden.
218
+ schema_map = {PLANAR_ENTITY_SCHEMA: self.entity_schema}
219
+ engine = create_async_engine(
220
+ url, execution_options={"schema_translate_map": schema_map}
221
+ )
206
222
 
207
223
  return engine
208
224
 
@@ -214,6 +230,12 @@ class DatabaseManager:
214
230
 
215
231
  db_backend = self.db_url.get_backend_name()
216
232
 
233
+ if self.entity_schema == PLANAR_SCHEMA:
234
+ logger.warning(
235
+ "entity_schema is set to 'planar'; mixing user and system tables in the same schema is discouraged",
236
+ entity_schema=self.entity_schema,
237
+ )
238
+
217
239
  match db_backend:
218
240
  case "sqlite":
219
241
  logger.info(
@@ -293,27 +315,23 @@ class DatabaseManager:
293
315
  else:
294
316
  # Ensure planar schema exists
295
317
  await conn.execute(text(f"CREATE SCHEMA IF NOT EXISTS {PLANAR_SCHEMA}"))
318
+ # Ensure the configured entity schema exists
319
+ if self.entity_schema != PLANAR_SCHEMA:
320
+ await conn.execute(
321
+ text(f"CREATE SCHEMA IF NOT EXISTS {self.entity_schema}")
322
+ )
296
323
 
297
- async def migrate(self, use_alembic: bool):
324
+ async def migrate(self):
298
325
  """
299
326
  Runs database migrations.
300
327
  By default, uses SQLModel.metadata.create_all.
301
- Set use_alembic=True to use Alembic (requires Alembic setup).
302
328
  """
303
329
  if not self.engine:
304
330
  raise RuntimeError("Database engine not initialized. Call connect() first.")
305
331
 
306
- logger.info("starting database migration")
307
- if use_alembic:
308
- logger.info("using alembic for migrations")
309
- await self._setup_database()
310
- await self._run_system_migrations()
311
- # For now user migrations are not supported, so we fall back to SQLModel.metadata.create_all
312
- async with self.engine.begin() as conn:
313
- await conn.run_sync(PLANAR_APPLICATION_METADATA.create_all)
314
-
315
- else:
316
- async with self.engine.begin() as conn:
317
- await self._setup_database()
318
- await conn.run_sync(PLANAR_FRAMEWORK_METADATA.create_all)
319
- await conn.run_sync(PLANAR_APPLICATION_METADATA.create_all)
332
+ logger.info("starting database migration with alembic")
333
+ await self._setup_database()
334
+ await self._run_system_migrations()
335
+ # For now user migrations are not supported, so we fall back to SQLModel.metadata.create_all
336
+ async with self.engine.begin() as conn:
337
+ await conn.run_sync(PLANAR_APPLICATION_METADATA.create_all)
@@ -19,15 +19,14 @@ from planar.workflows.decorators import workflow
19
19
  from planar.workflows.execution import execute
20
20
  from planar.workflows.models import Workflow
21
21
 
22
- app = PlanarApp(
23
- config=sqlite_config(":memory:"),
24
- title="Planar app for testing file workflows",
25
- description="Testing",
26
- )
27
-
28
22
 
29
23
  @pytest.fixture(name="app")
30
- def app_fixture():
24
+ def app_fixture(tmp_db_path: str):
25
+ app = PlanarApp(
26
+ config=sqlite_config(tmp_db_path),
27
+ title="Planar app for testing file workflows",
28
+ description="Testing",
29
+ )
31
30
  yield app
32
31
 
33
32
 
@@ -37,10 +37,10 @@ class TestAuditableModel(AuditableMixin, SQLModel, table=True):
37
37
 
38
38
 
39
39
  @pytest.fixture
40
- async def session(mem_db_engine):
40
+ async def session(tmp_db_engine):
41
41
  """Create a database session."""
42
42
 
43
- async with new_session(mem_db_engine) as session:
43
+ async with new_session(tmp_db_engine) as session:
44
44
  await (await session.connection()).run_sync(SQLModel.metadata.create_all)
45
45
  yield session
46
46
 
@@ -12,7 +12,10 @@ from .reexports import SQLModel
12
12
  logger = get_logger("orm.PlanarBaseEntity")
13
13
 
14
14
 
15
- PLANAR_APPLICATION_METADATA = MetaData()
15
+ # Default schema for all entity / user tables, but can be overridden by the user
16
+ # in planar configuration, which db.py uses.
17
+ PLANAR_ENTITY_SCHEMA = "planar_entity"
18
+ PLANAR_APPLICATION_METADATA = MetaData(schema=PLANAR_ENTITY_SCHEMA)
16
19
 
17
20
 
18
21
  class PlanarBaseEntity(UUIDPrimaryKeyMixin, AuditableMixin, SQLModel, table=False):
planar/py.typed ADDED
File without changes
@@ -15,10 +15,10 @@ from planar.testing.planar_test_client import PlanarTestClient
15
15
 
16
16
 
17
17
  @pytest.fixture(name="app")
18
- def app_fixture():
18
+ def app_fixture(tmp_db_path: str):
19
19
  """Create a test app with agents."""
20
20
  app = PlanarApp(
21
- config=sqlite_config(":memory:"),
21
+ config=sqlite_config(tmp_db_path),
22
22
  title="Test app for agent router",
23
23
  description="Testing agent endpoints",
24
24
  )
@@ -10,9 +10,9 @@ from planar.testing.planar_test_client import PlanarTestClient
10
10
 
11
11
 
12
12
  @pytest.fixture(name="app")
13
- def app_fixture():
13
+ def app_fixture(tmp_db_path: str):
14
14
  return PlanarApp(
15
- config=sqlite_config(":memory:"),
15
+ config=sqlite_config(tmp_db_path),
16
16
  title="Test app for files router",
17
17
  description="Testing files endpoints",
18
18
  )
@@ -41,10 +41,10 @@ class OutputFromTestRule(BaseModel):
41
41
 
42
42
 
43
43
  @pytest.fixture(name="app")
44
- def app_fixture():
44
+ def app_fixture(tmp_db_path: str):
45
45
  """Create a test app with agents and rules."""
46
46
  app = PlanarApp(
47
- config=sqlite_config(":memory:"),
47
+ config=sqlite_config(tmp_db_path),
48
48
  title="Test app for object config router",
49
49
  description="Testing object configuration endpoints",
50
50
  )
@@ -83,9 +83,9 @@ def pricing_rule_with_wrong_type(
83
83
 
84
84
 
85
85
  @pytest.fixture(name="app")
86
- def app_fixture():
86
+ def app_fixture(tmp_db_path: str):
87
87
  app = PlanarApp(
88
- config=sqlite_config(":memory:"),
88
+ config=sqlite_config(tmp_db_path),
89
89
  title="Test app for agent router",
90
90
  description="Testing agent endpoints",
91
91
  )
@@ -120,18 +120,16 @@ async def file_processing_workflow(file: PlanarFile):
120
120
  )
121
121
 
122
122
 
123
- app = PlanarApp(
124
- config=sqlite_config("test_workflow_router.db"),
125
- title="Test Workflow Router API",
126
- description="API for testing workflow routers",
127
- )
128
-
129
-
130
123
  # ------ TESTS ------
131
124
 
132
125
 
133
126
  @pytest.fixture(name="app")
134
- def app_fixture():
127
+ def app_fixture(tmp_db_path: str):
128
+ app = PlanarApp(
129
+ config=sqlite_config(tmp_db_path),
130
+ title="Test Workflow Router API",
131
+ description="API for testing workflow routers",
132
+ )
135
133
  # Re-register workflows since ObjectRegistry gets reset before each test
136
134
  app.register_workflow(expense_approval_workflow)
137
135
  app.register_workflow(file_processing_workflow)
@@ -1,5 +1,4 @@
1
1
  from planar.ai import Agent
2
- from planar.ai.providers import OpenAI
3
2
  from planar.files import PlanarFile
4
3
  from planar.human import Human
5
4
  from planar.rules.decorator import rule
@@ -23,7 +22,7 @@ class RuleOutput(BaseModel):
23
22
 
24
23
  invoice_agent = Agent(
25
24
  name="Invoice Agent",
26
- model=OpenAI.gpt_4_1,
25
+ model="openai:gpt-4.1",
27
26
  tools=[],
28
27
  max_turns=1,
29
28
  system_prompt="Extract vendor and amount from invoice text.",
@@ -3,8 +3,8 @@ name = "{{ name }}"
3
3
  version = "0.1.0"
4
4
  requires-python = ">=3.12"
5
5
  dependencies = [
6
- "planar>=0.6.0",
6
+ "planar>=0.9.0",
7
7
  ]
8
8
 
9
9
  [[tool.uv.index]]
10
- url = "https://coplane.github.io/planar/simple/"
10
+ url = "https://coplane.github.io/planar/simple/"
planar/test_sqlalchemy.py CHANGED
@@ -3,7 +3,7 @@ from uuid import uuid4
3
3
  import pytest
4
4
  from sqlalchemy.exc import DBAPIError
5
5
  from sqlalchemy.ext.asyncio import AsyncEngine
6
- from sqlmodel import col, insert, select
6
+ from sqlmodel import col, insert, select, text
7
7
 
8
8
  from planar.db import PlanarSession, new_session
9
9
  from planar.modeling.orm.planar_base_entity import PlanarBaseEntity
@@ -156,3 +156,38 @@ async def test_serializable_transaction_failure_1(tmp_db_engine: AsyncEngine):
156
156
  # Session 2: Commit should fail with serialization error
157
157
  with pytest.raises(DBAPIError, match="could not serialize access"):
158
158
  await session2.commit()
159
+
160
+
161
+ async def test_entity_schema_and_planar_schema_presence(tmp_db_engine: AsyncEngine):
162
+ table_name = SomeModel.__tablename__
163
+
164
+ async with new_session(tmp_db_engine) as session:
165
+ dialect = session.dialect.name
166
+
167
+ if dialect == "postgresql":
168
+ # Verify schemas include 'planar' and the default entity schema 'planar_entity'
169
+ res = await session.exec(
170
+ text("select schema_name from information_schema.schemata") # type: ignore[arg-type]
171
+ )
172
+ schemas = {row[0] for row in res}
173
+ assert "planar" in schemas
174
+ assert "planar_entity" in schemas
175
+
176
+ # Verify SomeModel table is created in the entity schema
177
+ res = await session.exec(
178
+ text(
179
+ "select table_schema from information_schema.tables where table_name = :tn"
180
+ ).bindparams(tn=table_name) # type: ignore[arg-type]
181
+ )
182
+ table_schemas = {row[0] for row in res}
183
+ assert "planar_entity" in table_schemas
184
+ assert "public" not in table_schemas
185
+
186
+ else:
187
+ # SQLite: no schemas; ensure table exists
188
+ res = await session.exec(
189
+ text("select name from sqlite_master where type='table'") # type: ignore[arg-type]
190
+ )
191
+ tables = {row[0] for row in res}
192
+ assert table_name in tables
193
+ assert not any(name.startswith("planar.") for name in tables)
@@ -254,23 +254,9 @@ async def tmp_db_engine(tmp_db_url: str):
254
254
  yield engine
255
255
 
256
256
 
257
- @pytest.fixture()
258
- async def mem_db_engine(tmp_db_engine):
259
- # Memory databases don't work well with aiosqlite due to the "database
260
- # table is locked" error (which doesn't respect timeouts), so just use a
261
- # temporary db file for now until we figure out a fix.
262
- yield tmp_db_engine
263
-
264
- # name = uuid4()
265
- # async with engine_context(
266
- # f"sqlite+aiosqlite:///file:{name}?mode=memory&cache=shared&uri=true"
267
- # ) as engine:
268
- # yield engine
269
-
270
-
271
257
  @pytest.fixture(name="session")
272
- async def session_fixture(mem_db_engine):
273
- async with new_session(mem_db_engine) as session:
258
+ async def session_fixture(tmp_db_engine):
259
+ async with new_session(tmp_db_engine) as session:
274
260
  tok = session_var.set(session)
275
261
  yield session
276
262
  session_var.reset(tok)
@@ -318,7 +304,7 @@ async def tracer_fixture():
318
304
  async def engine_context(url: str):
319
305
  db_manager = DatabaseManager(url)
320
306
  db_manager.connect()
321
- await db_manager.migrate(use_alembic=True)
307
+ await db_manager.migrate()
322
308
  engine = db_manager.get_engine()
323
309
  tok = engine_var.set(engine)
324
310
  yield engine
@@ -26,7 +26,7 @@ async def long_running_workflow():
26
26
  return "finished"
27
27
 
28
28
 
29
- async def test_lock_timer_extension(mem_db_engine):
29
+ async def test_lock_timer_extension(tmp_db_engine):
30
30
  tracer = SynchronizableTracer()
31
31
  tracer_var.set(tracer)
32
32
  lock_acquired = tracer.instrument(
@@ -36,7 +36,7 @@ async def test_lock_timer_extension(mem_db_engine):
36
36
  TraceSpec(function_name="lock_heartbeat", message="commit")
37
37
  )
38
38
 
39
- async with new_session(mem_db_engine) as session:
39
+ async with new_session(tmp_db_engine) as session:
40
40
  # This test verifies that when a workflow is executing, the heartbeat task
41
41
  # (lock_heartbeat) extends the workflow's lock_until field. We run a
42
42
  # long-running workflow (which sleeps for 1 second) with a short lock
@@ -95,7 +95,7 @@ async def crashed_worker_workflow():
95
95
  return "completed"
96
96
 
97
97
 
98
- async def test_orchestrator_resumes_crashed_worker(mem_db_engine):
98
+ async def test_orchestrator_resumes_crashed_worker(tmp_db_engine):
99
99
  # This test simulates the scenario where a worker has “crashed” after
100
100
  # locking a workflow. We start a workflow that suspends. Then we add a LockedResource
101
101
  # record with an expired lock_until time to simulate a crashed
@@ -105,7 +105,7 @@ async def test_orchestrator_resumes_crashed_worker(mem_db_engine):
105
105
  # the workflow to be resumed. Finally, we verify that the workflow
106
106
  # completes successfully. Start the workflow – its first execution will
107
107
  # suspend.
108
- async with new_session(mem_db_engine) as session:
108
+ async with new_session(tmp_db_engine) as session:
109
109
  session_var.set(session)
110
110
  wf = await crashed_worker_workflow.start()
111
111
 
@@ -1,9 +1,8 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: planar
3
- Version: 0.9.0
3
+ Version: 0.9.2
4
4
  Summary: Add your description here
5
5
  License-Expression: LicenseRef-Proprietary
6
- Requires-Python: >=3.12
7
6
  Requires-Dist: aiofiles>=24.1.0
8
7
  Requires-Dist: aiosqlite>=0.21.0
9
8
  Requires-Dist: alembic>=1.14.1
@@ -23,19 +22,20 @@ Requires-Dist: sqlmodel>=0.0.22
23
22
  Requires-Dist: typer>=0.15.2
24
23
  Requires-Dist: typing-extensions>=4.12.2
25
24
  Requires-Dist: zen-engine>=0.40.0
25
+ Requires-Dist: azure-storage-blob>=12.19.0 ; extra == 'azure'
26
+ Requires-Dist: azure-identity>=1.15.0 ; extra == 'azure'
27
+ Requires-Dist: aiohttp>=3.8.0 ; extra == 'azure'
28
+ Requires-Dist: ducklake>=0.1.1 ; extra == 'data'
29
+ Requires-Dist: ibis-framework[duckdb]>=10.8.0 ; extra == 'data'
30
+ Requires-Dist: polars>=1.31.0 ; extra == 'data'
31
+ Requires-Dist: opentelemetry-api>=1.34.1 ; extra == 'otel'
32
+ Requires-Dist: opentelemetry-exporter-otlp>=1.34.1 ; extra == 'otel'
33
+ Requires-Dist: opentelemetry-instrumentation-logging>=0.55b1 ; extra == 'otel'
34
+ Requires-Dist: opentelemetry-sdk>=1.34.1 ; extra == 'otel'
35
+ Requires-Python: >=3.12
26
36
  Provides-Extra: azure
27
- Requires-Dist: aiohttp>=3.8.0; extra == 'azure'
28
- Requires-Dist: azure-identity>=1.15.0; extra == 'azure'
29
- Requires-Dist: azure-storage-blob>=12.19.0; extra == 'azure'
30
37
  Provides-Extra: data
31
- Requires-Dist: ducklake>=0.1.1; extra == 'data'
32
- Requires-Dist: ibis-framework[duckdb]>=10.8.0; extra == 'data'
33
- Requires-Dist: polars>=1.31.0; extra == 'data'
34
38
  Provides-Extra: otel
35
- Requires-Dist: opentelemetry-api>=1.34.1; extra == 'otel'
36
- Requires-Dist: opentelemetry-exporter-otlp>=1.34.1; extra == 'otel'
37
- Requires-Dist: opentelemetry-instrumentation-logging>=0.55b1; extra == 'otel'
38
- Requires-Dist: opentelemetry-sdk>=1.34.1; extra == 'otel'
39
39
  Description-Content-Type: text/markdown
40
40
 
41
41
  # Planar
@@ -61,8 +61,8 @@ The workflow system in Planar is a sophisticated orchestration framework that en
61
61
  - Concurrency Control: Uses a locking mechanism to prevent multiple executions
62
62
  - Recovery: Can recover from crashes by detecting stalled workflows
63
63
  3. Main Components:
64
- - @workflow decorator: Marks a function as a workflow with persistence
65
- - @step decorator: Wraps function calls inside a workflow to make them resumable
64
+ - `@workflow` decorator: Marks a function as a workflow with persistence
65
+ - `@step` decorator: Wraps function calls inside a workflow to make them resumable
66
66
  - Suspend class: Allows pausing workflow execution
67
67
  - workflow_orchestrator: Background task that finds and resumes suspended workflows
68
68
  4. REST API Integration:
@@ -78,18 +78,20 @@ The workflow system in Planar is a sophisticated orchestration framework that en
78
78
 
79
79
  Planar builds on Python's async/await system but adds durability. When you create a workflow:
80
80
 
81
+ ```python
81
82
  @workflow
82
83
  async def process_order(order_id: str):
83
84
  # workflow steps
85
+ ```
84
86
 
85
87
  The system:
86
88
 
87
- 1. Enforces that all workflows and steps must be coroutines (async def)
88
- 2. Accesses the underlying generator of the coroutine via coro.__await__()
89
- 3. Manually drives this generator by calling next(gen) and gen.send(result)
89
+ 1. Enforces that all workflows and steps must be coroutines (`async def`)
90
+ 2. Accesses the underlying generator of the coroutine via `coro.__await__()`
91
+ 3. Manually drives this generator by calling `next(gen)` and `gen.send(result)`
90
92
  4. Intercepts any values yielded from the coroutine to implement suspension
91
93
 
92
- The execute() function (lines 278-335) is the core that drives coroutine execution. It:
94
+ The `execute()` function (lines 278-335) is the core that drives coroutine execution. It:
93
95
  - Takes control of the coroutine's generator
94
96
  - Processes each yielded value
95
97
  - Handles regular awaits vs. suspensions differently
@@ -99,6 +101,7 @@ The workflow system in Planar is a sophisticated orchestration framework that en
99
101
 
100
102
  The Suspend class (lines 55-72) enables pausing workflows:
101
103
 
104
+ ```python
102
105
  class Suspend:
103
106
  def __init__(self, *, wakeup_at=None, interval=None):
104
107
  # Set when to wake up
@@ -106,36 +109,42 @@ The workflow system in Planar is a sophisticated orchestration framework that en
106
109
  def __await__(self):
107
110
  result = yield self
108
111
  return result
112
+ ```
109
113
 
110
114
  When you call:
115
+ ```python
111
116
  await suspend(interval=timedelta(minutes=5))
117
+ ```
112
118
 
113
119
  What happens:
114
- 1. The suspend() function uses the @step() decorator to mark it as resumable
120
+ 1. The `suspend()` function uses the `@step()` decorator to mark it as resumable
115
121
  2. Inside it creates and awaits a Suspend object
116
- 3. The __await__ method yields self (the Suspend instance) to the executor
117
- 4. The execute() function detects this is a Suspend object (lines 303-307)
122
+ 3. The `__await__` method yields self (the Suspend instance) to the executor
123
+ 4. The `execute()` function detects this is a Suspend object (lines 303-307)
118
124
  5. It sets the workflow status to SUSPENDED and persists the wake-up time
119
- 6. Later, the orchestrator finds workflows ready to resume based on wakeup_at
125
+ 6. Later, the orchestrator finds workflows ready to resume based on `wakeup_at`
120
126
  7. When resumed, execution continues right after the suspension point
121
127
 
122
128
  YieldWrapper
123
129
 
124
130
  The YieldWrapper class (lines 48-53) is crucial for handling regular async operations:
125
131
 
132
+ ```python
126
133
  class YieldWrapper:
127
134
  def __init__(self, value):
128
135
  self.value = value
129
136
  def __await__(self):
130
137
  return (yield self.value)
138
+ ```
131
139
 
132
140
  For non-Suspend yields (regular awaits), the system:
133
- 1. Wraps the yielded value in YieldWrapper
134
- 2. Awaits it to get the result from asyncio
141
+ 1. Wraps the yielded value in `YieldWrapper`
142
+ 2. Awaits it to get the result from `asyncio`
135
143
  3. Sends the result back to the workflow's generator
136
144
 
137
145
  This allows you to use normal async functions inside workflows:
138
146
 
147
+ ```python
139
148
  @workflow
140
149
  async def my_workflow():
141
150
  # This works because YieldWrapper passes through regular awaits
@@ -144,6 +153,7 @@ The workflow system in Planar is a sophisticated orchestration framework that en
144
153
  await suspend(interval=timedelta(hours=1))
145
154
  # When resumed days later, continues here
146
155
  return process_result(data)
156
+ ```
147
157
 
148
158
  The magic is that the workflow appears to be a normal async function, but the state is persisted across suspensions, allowing workflows to survive
149
159
  process restarts or even server reboots.
@@ -1,43 +1,29 @@
1
1
  planar/__init__.py,sha256=FAYRGjuJOH2Y_XYFA0-BrRFjuKdPzIShNbaYwJbtu6A,499
2
- planar/_version.py,sha256=4bF_N3mS34eGDXoMdYuijyM1JYXxzz7J-NCugplemp0,18
3
- planar/app.py,sha256=1X0ZPW7nmf6RhMzQgjtyKD_WO-E4tjM7ItOP-bjvocQ,18505
4
- planar/cli.py,sha256=2ObR5XkLGbdbnDqp5mrBzDVhSacHCNsVNSHnXkrMQzQ,9593
5
- planar/config.py,sha256=ghZNDqncwPK3LjvkMkv0BNnN1w-7ALpDBZWc1-JanTE,17634
6
- planar/dependencies.py,sha256=PH78fGk3bQfGnz-AphxH49307Y0XVgl3EY0LdGJnoik,1008
7
- planar/object_registry.py,sha256=RMleX5XE8OKDxlnMeyLpJ1Y280duub-tx1smR1zTlDg,3219
8
- planar/registry_items.py,sha256=UhZRIpbSoa_CV9OTl17pJfRLxItYp4Pxd9f5ZbJkGaM,2055
9
- planar/session.py,sha256=xLS9WPvaiy9nr2Olju1-C-7_sU5VXK8RuNdjuKndul4,1020
10
- planar/task_local.py,sha256=pyvT0bdzAn15HL2yQUs9YrU5MVXh9njQt9MH51AGljs,1102
11
- planar/test_app.py,sha256=5dYhOW6lRbAx2X270DfqktkJ5IfuqfowX6bwxM1WQAM,4865
12
- planar/test_cli.py,sha256=faR6CSuooHHyyB5Yt-p8CIr7mGtKrrU2TLQbc4Oe9bA,13834
13
- planar/test_config.py,sha256=HcmDu1nwKZZhzHQLGVyP9oxje-_g_XubEsvzRj28QPg,14328
14
- planar/test_object_config.py,sha256=izn4s2HmSDWpGtgpOTDmKeUYN2-63WDR1QtVQrT-x00,20135
15
- planar/test_object_registry.py,sha256=R7IwbB2GACm2HUuVZTeVY4V12XB9_JgSSeppPxiCdfs,480
16
- planar/test_sqlalchemy.py,sha256=F0aKqm5tStQj_Mqjh50kiLX4r7kphBFDOUDu_Iw7S3s,5573
17
- planar/test_utils.py,sha256=gKenXotj36SN_bb3bQpYPfD8t06IjnGBQqEgWpujHcA,3086
18
- planar/utils.py,sha256=v7q9AJyWgQWl9VPSN_0qxw3rBvYe-_Pb_KcwqSsjOFU,3103
19
2
  planar/ai/__init__.py,sha256=ABOKvqQOLlVJkptcvXcuLjVZZWEsK8h-1RyFGK7kib8,231
20
3
  planar/ai/agent.py,sha256=flgHU00LRT-UcP0TjMqDigi2jwWq6UoMpmCZSOTyyB0,12428
21
- planar/ai/agent_base.py,sha256=iOOiUwbTiqckrZ-ZtlpkPCjSNE117gMwxrdgegO-P-0,5303
22
- planar/ai/agent_utils.py,sha256=Yug1lt3uT7zLJ0X9uUBpKEomxucKaZiEUBIcf-RZILo,4052
23
- planar/ai/models.py,sha256=aH61vkHJEhmupvGJHS87Nv7bpCpcfBJDO-N8k3k2ixc,4292
24
- planar/ai/pydantic_ai.py,sha256=lYWtnIclOLRiEpBJi5r6Ey8gDBVlQIHTFa3iEzUNqWY,23525
4
+ planar/ai/agent_base.py,sha256=rdK5ExCpkPf5sdVy-Wo5MKAx2O_GULFCwA24s0XO6Ek,5462
5
+ planar/ai/agent_utils.py,sha256=MYNerdAm2TPVbDSKAmBCUlGmR56NAc8seZmDAFOWvUA,4199
6
+ planar/ai/models.py,sha256=bZd4MoBBJMqzXJqsmsbMdZtOaRrNeX438CHAqOvmpfw,4598
7
+ planar/ai/pydantic_ai.py,sha256=FpD0pE7wWNYwmEUZ90D7_J8gbAoqKmWtrLr2fhAd7rg,23503
25
8
  planar/ai/test_agent_serialization.py,sha256=zYLIxhYdFhOZzBrEBoQNyYLyNcNxWwaMTkjt_ARTkZk,8073
26
9
  planar/ai/utils.py,sha256=WVBW0TGaoKytC4bNd_a9lXrBf5QsDRut4GBcA53U2Ww,3116
10
+ planar/app.py,sha256=VEs4jDlcisyOy9I9zEGMG_-Qm8ULKT36CSHjqrYit3o,18491
11
+ planar/cli.py,sha256=2ObR5XkLGbdbnDqp5mrBzDVhSacHCNsVNSHnXkrMQzQ,9593
12
+ planar/config.py,sha256=6J42G9rEVUiOyCAY3EwUTU3PPmWthGTnrHMzST9TMcc,17809
27
13
  planar/data/__init__.py,sha256=LwrWl925w1CN0aW645Wpj_kDp0B8j5SsPzjr9iyrcmI,285
28
14
  planar/data/config.py,sha256=zp6ChI_2MUMbupEVQNY-BxzcdLvejXG33DCp0BujGVU,1209
29
- planar/data/dataset.py,sha256=5SdOh7NzJdTpuleNkoA_UA-C4WJM8mcNLcvGsRIiqIs,10150
15
+ planar/data/dataset.py,sha256=P0NVE2OvJcXMKqVylYczY2lSGR0pSWlPAHM_upKoBWQ,9507
30
16
  planar/data/exceptions.py,sha256=AlhGQ_TReyEzfPSlqoXCjoZ1206Ut7dS4lrukVfGHaw,358
31
17
  planar/data/test_dataset.py,sha256=w2kay2PE-BhkryM3cOKX0nzSr2G0nCJxDuW1QCeFbyk,9985
32
18
  planar/db/__init__.py,sha256=SNgB6unQ1f1E9dB9O-KrsPsYM17KLsgOW1u0ajqs57I,318
33
- planar/db/alembic.ini,sha256=8G9IWbmF61Vwp1BXbkNOXTTgCEUMBQhOK_e-nnpnSYY,4309
34
- planar/db/db.py,sha256=dELx4CHZkOAxFgHsW8qPlx2kUZ8J7cTlU6rHljMV_vg,12102
35
- planar/db/alembic/env.py,sha256=cowI6O_4BMJPqDAukkbg69lzdsE44soi3ysxKGXbS_w,5207
36
- planar/db/alembic/script.py.mako,sha256=Cl7ixgLNtLk1gF5xFNXOnC9YYLX4cpFd8yHtEyY0_dY,699
19
+ planar/db/alembic/env.py,sha256=UlOrLBfFJ-WbNK0R1cgS2MC3yrqeE4-6rIirB3rGLYo,5344
20
+ planar/db/alembic/script.py.mako,sha256=BgXfi4ClINnJU-PaaWqh1-Sjqu4brkWpbVd-0rEPzLU,665
37
21
  planar/db/alembic/versions/3476068c153c_initial_system_tables_migration.py,sha256=1FbzJyfapjegM-Mxd3HMMVA-8zVU6AnrnzEgIoc6eoQ,13204
22
+ planar/db/alembic.ini,sha256=8G9IWbmF61Vwp1BXbkNOXTTgCEUMBQhOK_e-nnpnSYY,4309
23
+ planar/db/db.py,sha256=VNpHH1R62tdWVLIV1I2ULmw3B8M6-RsM2ALG3VAVjSg,12790
24
+ planar/dependencies.py,sha256=PH78fGk3bQfGnz-AphxH49307Y0XVgl3EY0LdGJnoik,1008
38
25
  planar/files/__init__.py,sha256=fms64l32M8hPK0SINXxNCykr2EpjBTcdgnezVgaCwkc,120
39
26
  planar/files/models.py,sha256=zbZvMkoqoSnn7yOo26SRtEgtlHJbFIvwSht75APHQXk,6145
40
- planar/files/test_files.py,sha256=2GFAz39dIf6-bhmJaDFeOpS2sZN7VnLI60ky-e170Mc,8921
41
27
  planar/files/storage/azure_blob.py,sha256=PzCm8ZpyAMH9-N6VscTlLpud-CBLcQX9qC6YjbOSfZg,12316
42
28
  planar/files/storage/base.py,sha256=KO7jyKwjKg5fNSLvhxJWE-lsypv6LXXf7bgA34aflwY,2495
43
29
  planar/files/storage/config.py,sha256=jE9Dn6cG_a4x9pdaZkasOxjyWkK6hmplLrPjEsRXGLM,3473
@@ -47,6 +33,7 @@ planar/files/storage/s3.py,sha256=1861rSw3kplXtugUWD7mdSD_EnPSHME1mGc82V69r5g,82
47
33
  planar/files/storage/test_azure_blob.py,sha256=OFYpns6JyeCCBHCoLz56uUHR6tWWeSZldUant5llczI,14200
48
34
  planar/files/storage/test_local_directory.py,sha256=KtzRfjtZUew1U-KETtD2mb6ywwX6HmjzaaeixOP0Ebg,5751
49
35
  planar/files/storage/test_s3.py,sha256=QG-CH7fiaRmQRwffnqG2mLRrw9LIlR2-xRyHs6Wuspo,10565
36
+ planar/files/test_files.py,sha256=nclsbLnbijCWQ-Aj8Yvo06hs72PygL1Wps7uk7716sc,8957
50
37
  planar/human/__init__.py,sha256=FwpV-FFssKKlvKSjWoI4gJB1XTMaNb1UNCSBxjAtIBw,147
51
38
  planar/human/human.py,sha256=-oRtN_8bCtSV7Sxku7yG4rof7T5pr4j18Cfm3u4Z3PM,14925
52
39
  planar/human/models.py,sha256=Cec1Y9NGGtuAl1ZhqNc9PWIq__BbiWVTh7IYKR4yl3w,2317
@@ -63,18 +50,21 @@ planar/modeling/field_helpers.py,sha256=9SOHTWPzjlaiq7RF88wjug3NvAywFurcHn651YL_
63
50
  planar/modeling/json_schema_generator.py,sha256=NDqPkWQA_I7ywQXCEQfj5ub9u5KAFEcSQpXVkrCluV4,2864
64
51
  planar/modeling/mixins/__init__.py,sha256=Lwg5eL4VFfv61FRBvH5OZqIyfrSogxQlYLUDnWnSorg,320
65
52
  planar/modeling/mixins/auditable.py,sha256=WP7aDWVn1j22ZffKzYRpu23JQJ4vvHCU1qxcbgChwhc,1619
66
- planar/modeling/mixins/test_auditable.py,sha256=zIa63_0Y19wMF7oIvMcOj7RoIVH7ztQzis9kHFEKKR8,2912
53
+ planar/modeling/mixins/test_auditable.py,sha256=RSYesWWBysFEWTD39-yDhww3wCe1OPN9Yt3ywGmx4d8,2912
67
54
  planar/modeling/mixins/test_timestamp.py,sha256=oLKPvr8oUdjJPJRif81nn4YV_uwbxql_ojjXdKI7j7E,4366
68
55
  planar/modeling/mixins/test_uuid_primary_key.py,sha256=t9ZoB0dS4jjJVrHic7EeEh_g3eZeYV3mr0ylv3Kr1Io,1575
69
56
  planar/modeling/mixins/timestamp.py,sha256=-eHndCWztDiOxfCI2UknmphGeoHMJVDJG1Lz4KtkQUA,1641
70
57
  planar/modeling/mixins/uuid_primary_key.py,sha256=O1BtuXk5GdsfpwTS6nGZm1GNi0pdXKQ6Kt2f5ZjiuMc,453
71
58
  planar/modeling/orm/__init__.py,sha256=QwPgToKEf_gCdjAjKKmgh0xLTHGsboK1kH1a1a0tSy0,339
72
- planar/modeling/orm/planar_base_entity.py,sha256=nL1kT2k__F9Lmo1SW2XEfB6vJi_Fg72u-wW1F79eFTs,959
59
+ planar/modeling/orm/planar_base_entity.py,sha256=7X0d86mWABleTWpulnA91P80ZjlUdGd6bv-L_TIhR2Q,1151
73
60
  planar/modeling/orm/query_filter_builder.py,sha256=Q2qWojo1Pzkt3HY1DdkBINlsP7uTOg1OOSUmwlzjTs8,5335
74
61
  planar/modeling/orm/reexports.py,sha256=sP7nw8e1yp1cahpfsefO84P5n4TNnBRk1jVHuCuH4U4,290
75
62
  planar/object_config/__init__.py,sha256=8LbI3teg3jCKoUznZ7cal22C1URnHtJMpBokCHZQUWo,352
76
63
  planar/object_config/models.py,sha256=nCyK82JitZwzGwbaBa-dZVxHPnL51ZJ6h87a-KEwHAw,3078
77
64
  planar/object_config/object_config.py,sha256=MgaL-jBFJJtP6ipZ2eJs-KMhj94V_sT3QCSoVTpYP3Y,13609
65
+ planar/object_registry.py,sha256=RMleX5XE8OKDxlnMeyLpJ1Y280duub-tx1smR1zTlDg,3219
66
+ planar/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
67
+ planar/registry_items.py,sha256=UhZRIpbSoa_CV9OTl17pJfRLxItYp4Pxd9f5ZbJkGaM,2055
78
68
  planar/routers/__init__.py,sha256=B_ZEbBuosX4ahPfvWZsyMIPmQm0rt6ail4nJA6NLfOk,379
79
69
  planar/routers/agents_router.py,sha256=trb1JPYVlaV7O2uoYvKIrLuTNGP_PmQSLZmXYFWrHkg,8251
80
70
  planar/routers/entity_router.py,sha256=7Y1LDSqI_ovoOGr9DGylGM8BmRxF-WSPQSwITJHc6NE,4841
@@ -85,19 +75,18 @@ planar/routers/info.py,sha256=HQa-mumw4zitG61V9isJlZ3cMr8pEwlB54Ct_LrpJDo,4473
85
75
  planar/routers/models.py,sha256=RwXjXpJw2uyluM4Fjc34UA0Jm7J95cUjbmTTarD_P9k,4669
86
76
  planar/routers/object_config_router.py,sha256=zA8-gGBQp1-Gm3uCC4WJ6nLicFwt4CsCqCYLFp1lRN8,4802
87
77
  planar/routers/rule.py,sha256=d6giUwYRKzxQFPeoWbe8Ylp2Cxd71_uK8yoS9NrOOBg,3563
88
- planar/routers/test_agents_router.py,sha256=d_d_lZT5zuSxNY2MEu51SmgLRGNZ3yCpGUooAXLpEaY,6082
89
- planar/routers/test_files_router.py,sha256=_uYpRJkxSxyjFJAG7aj3letx25iDSkaOgZDTRHfU8TU,1559
90
- planar/routers/test_object_config_router.py,sha256=HBOsQZXccPuWOLCPxEsduSd93loswUsbSk3eTM6KHRc,11389
78
+ planar/routers/test_agents_router.py,sha256=jzRCLB21YcEfhaFUos_gfRp9WDdP38_cozTQkHbi9b4,6099
79
+ planar/routers/test_files_router.py,sha256=HfZF1zeJ9BD2jhE6s698Jo7sDpO55RJF5g1Ksup4jtM,1576
80
+ planar/routers/test_object_config_router.py,sha256=JpzoNlNONgljmGJYtrXnhtGhKjUT3YQMhP89fnt7dn4,11406
91
81
  planar/routers/test_routes_security.py,sha256=lXHeYg_th4UaDWeANM-dzONF8p2bEtwXJYYUlftE9R8,5556
92
- planar/routers/test_rule_router.py,sha256=gCJO7-NVjkio-0ZHY2hNgPvubN-0NAPA3Hg5Jcrwe70,17203
93
- planar/routers/test_workflow_router.py,sha256=rjK1Eau-YWs3vZbuJ50Ae_8I8_8TfxQA0F8I2HeDl9o,16572
82
+ planar/routers/test_rule_router.py,sha256=08fa4sc7RaXvQzPCQQ4LaftfXuQwoPEDzcS4lesPG2Q,17220
83
+ planar/routers/test_workflow_router.py,sha256=w3Gl1Okr2FgKMIqcum3gD6XzVrYm82oSIj_znbtd-aQ,16592
94
84
  planar/routers/workflow.py,sha256=8R35ENZeB5Mdt7WfH2zZ75BPg2oQ9d8kL47P1qvP-7Q,18045
95
85
  planar/rules/__init__.py,sha256=lF3F8Rdf2ottjiJu0IeBdqhg1bckLhOqZFI2t-8KItM,474
96
86
  planar/rules/decorator.py,sha256=nxT17n9uwfXMOlk5lliw_cRS7Y83gMI6CQdrf_pB5yk,6666
97
87
  planar/rules/models.py,sha256=vC38JLeGzmU87L8BX4AyVJLJHmRYjWRmoHQ6S6ZlhPg,10186
98
88
  planar/rules/rule_configuration.py,sha256=B2G6mPnfxA277nF-Gr-B_Uely-ZOhz2jAhiwQMZuY-k,6508
99
89
  planar/rules/runner.py,sha256=KIPrt_ri50qotvDLOY9xly40bNTWRh8GVT2kEJFFtFo,1714
100
- planar/rules/test_rules.py,sha256=6M7CSg1bwn7O7DOoNi38vyVG4UmPQfRFxEO9qGE6rz0,52011
101
90
  planar/rules/test_data/account_dormancy_management.json,sha256=9aMMELZrF5DTBluMKUXJptxwULEcva4GHEyaapIeerY,4776
102
91
  planar/rules/test_data/airline_loyalty_points_calculator.json,sha256=7S1koMe60yR3h2VQys34oLy5ynhsEQ5wadMLPHCRQZA,5689
103
92
  planar/rules/test_data/applicant_risk_assessment.json,sha256=rj-Q13NczdNt00x5wrvGLalw5IfdT1j-_RvpwCZa7Fc,9994
@@ -112,13 +101,14 @@ planar/rules/test_data/order_consolidation_system.json,sha256=kWJuVHAfAqsDW2xVdx
112
101
  planar/rules/test_data/portfolio_risk_monitor.json,sha256=tTvQOJJLhakGxG4CnA9fdBIECstJnp0B8ogFADkdy8s,15168
113
102
  planar/rules/test_data/supply_chain_risk.json,sha256=fO0wV5ZnsZQpOP19Zp2troTMADaX0-KMpCxG_uHG198,7263
114
103
  planar/rules/test_data/warehouse_cross_docking.json,sha256=IPfcgNkY2sds301BeW6CjgFtK_zRyr27gI3UcqCB2Uo,5549
104
+ planar/rules/test_rules.py,sha256=6M7CSg1bwn7O7DOoNi38vyVG4UmPQfRFxEO9qGE6rz0,52011
105
+ planar/scaffold_templates/app/__init__.py.j2,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
106
+ planar/scaffold_templates/app/db/entities.py.j2,sha256=wg9O3JtRaRMKlDtoWHHodyNRL0s1UILvsr9fCQ_O2-4,279
107
+ planar/scaffold_templates/app/flows/process_invoice.py.j2,sha256=R3EII_O2DHV1kvffW_AApZyaS6rR9eikcpxI08XH9dI,1691
115
108
  planar/scaffold_templates/main.py.j2,sha256=HcV0PVzcyRDaJvNdDQIFiDR1MJlLquNQzNO9oNkCKDQ,322
116
109
  planar/scaffold_templates/planar.dev.yaml.j2,sha256=I5-IqX7GJm6qA91WtUMw43L4hKACqgnER_H2racim4c,998
117
110
  planar/scaffold_templates/planar.prod.yaml.j2,sha256=FahJ2atDtvVH7IUCatGq6h9hmyF8meeiWC8RLfWphOQ,867
118
- planar/scaffold_templates/pyproject.toml.j2,sha256=zD8UaEwK3uUYoGQuFuPZZCNdvqNS0faGrNHbD081DAU,182
119
- planar/scaffold_templates/app/__init__.py.j2,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
120
- planar/scaffold_templates/app/db/entities.py.j2,sha256=wg9O3JtRaRMKlDtoWHHodyNRL0s1UILvsr9fCQ_O2-4,279
121
- planar/scaffold_templates/app/flows/process_invoice.py.j2,sha256=nVJ5BlhOkHFEGkBuQNEF5G2P5HA1FGx25NSS5RXBUsw,1728
111
+ planar/scaffold_templates/pyproject.toml.j2,sha256=nFfHWLp0sFK8cqjkdwBm6Hi6xsPzTNkaBeSgdTWTS-Q,183
122
112
  planar/security/auth_context.py,sha256=i63JkHQ3oXNlTis7GIKRkZJbkcvZhD2jVDuO7blgbSc,5068
123
113
  planar/security/auth_middleware.py,sha256=Grrm0i2bstWZ83ukrNZsHvFbNzffN0rvbbCcb2OxRY0,5746
124
114
  planar/security/authorization.py,sha256=zoej88_VINVNSDXm7u2LJbwOpMqmXBKj_pmCaPTar7M,11721
@@ -129,18 +119,28 @@ planar/security/tests/test_authorization_context.py,sha256=cnsC3V13NBJwzyIwZaM9w
129
119
  planar/security/tests/test_cedar_basics.py,sha256=i1jLPjlJT1n_97onbeDYVpnwAzU2PmHvIPvaJSH1J2U,1026
130
120
  planar/security/tests/test_cedar_policies.py,sha256=-Vn_CQgCUAVg7YhdUd34FsOjNL1EmY_o92r-fzmknP8,4848
131
121
  planar/security/tests/test_jwt_principal_context.py,sha256=nGElTLtXbabkAxd3kXVpSFdH7kvSzHzSkp89g5Vu5Hc,4691
122
+ planar/session.py,sha256=xLS9WPvaiy9nr2Olju1-C-7_sU5VXK8RuNdjuKndul4,1020
132
123
  planar/sse/constants.py,sha256=jE3SooTEWPuuL_Bi6DisJYMR9pKOiHVfboU2h5QTJRg,22
133
124
  planar/sse/example.html,sha256=SgTJbdJ3B1F1DxLC2YWuX2F1XVwKcTjX34CbJCXoCTM,4144
134
125
  planar/sse/hub.py,sha256=5jhfk7zdCivau3TT1MxU2qtvETSskhqEiXzt-t0sRpE,6859
135
126
  planar/sse/model.py,sha256=fU_Fx9LS2ouS6-Dj1TIF-PLGul9YratKWafoWfZR1gc,123
136
127
  planar/sse/proxy.py,sha256=aJGo_-JIeQ0xSmE4HJdulZxIgCVRsBMMXqqSqtPvTvo,9177
128
+ planar/task_local.py,sha256=pyvT0bdzAn15HL2yQUs9YrU5MVXh9njQt9MH51AGljs,1102
129
+ planar/test_app.py,sha256=5dYhOW6lRbAx2X270DfqktkJ5IfuqfowX6bwxM1WQAM,4865
130
+ planar/test_cli.py,sha256=faR6CSuooHHyyB5Yt-p8CIr7mGtKrrU2TLQbc4Oe9bA,13834
131
+ planar/test_config.py,sha256=HcmDu1nwKZZhzHQLGVyP9oxje-_g_XubEsvzRj28QPg,14328
132
+ planar/test_object_config.py,sha256=izn4s2HmSDWpGtgpOTDmKeUYN2-63WDR1QtVQrT-x00,20135
133
+ planar/test_object_registry.py,sha256=R7IwbB2GACm2HUuVZTeVY4V12XB9_JgSSeppPxiCdfs,480
134
+ planar/test_sqlalchemy.py,sha256=QTloaipWiFmlLTBGH6YCRkwi1R27gmQZnwprO7lPLfU,7058
135
+ planar/test_utils.py,sha256=gKenXotj36SN_bb3bQpYPfD8t06IjnGBQqEgWpujHcA,3086
137
136
  planar/testing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
138
- planar/testing/fixtures.py,sha256=UEg12Ohj2Oip94CrC_wQ9rch71wctUZ1fYNryJLsPUY,9149
137
+ planar/testing/fixtures.py,sha256=spK7iL1NSv-d8fd139ep-SDogZR2ZycGkD_voSAPPF4,8662
139
138
  planar/testing/memory_storage.py,sha256=apcuFisC3hW9KiU3kO8zwHQ6oK9Lu20NSX5fJ0LSZUY,2824
140
139
  planar/testing/planar_test_client.py,sha256=qPkI_ZHZho_38PpdSmEjcRBO1iHcIx3dOwo7c02Am10,1979
141
140
  planar/testing/synchronizable_tracer.py,sha256=SWeta1CgwGsN5duC0FR8NyXOQ1b1L8nDpvGdjZVJ9Bg,4938
142
141
  planar/testing/test_memory_storage.py,sha256=So32XL0gbLDFMTl-WJN445x9jL6O8Qsqw8IRaiZnsPs,4797
143
142
  planar/testing/workflow_observer.py,sha256=0Q2xsYuZzNGXHZVwvXBqL9KXPsdIXuSZGBJAxHopzJw,2976
143
+ planar/utils.py,sha256=v7q9AJyWgQWl9VPSN_0qxw3rBvYe-_Pb_KcwqSsjOFU,3103
144
144
  planar/workflows/__init__.py,sha256=yFrrtKYUCx4jBPpHdEWDfKQgZXzGyr9voj5lFe9C-_w,826
145
145
  planar/workflows/context.py,sha256=93kPSmYniqjX_lv6--eUUPnzZEKZJi6IPaAjrT-hFRY,1271
146
146
  planar/workflows/contrib.py,sha256=b7WhCancxNCKO63mJCez9MahwMQc5_3zQxr_soJoXCY,6478
@@ -160,13 +160,13 @@ planar/workflows/step_metadata.py,sha256=7hwcIm6ot8m-iUXSYCbPmkg6bWegF6_RJ1stInv
160
160
  planar/workflows/step_testing_utils.py,sha256=WiTwxB4mM2y6dW7CJ3PlIR1BkBodSxQV7-S25pQ3Ycs,2361
161
161
  planar/workflows/sub_workflow_runner.py,sha256=EpS7DhhXRbC6ABm-Sho6Uyxh2TqCjcTPDYvcTQN4FjY,8313
162
162
  planar/workflows/test_concurrency_detection.py,sha256=yfgvLOMkPaK7EiW4ihm1KQx82Y-s9pB6uJhBfDi7PwQ,4528
163
- planar/workflows/test_lock_timeout.py,sha256=WAQttTjmetTLiDHb9j0o_ydvF6j4TV1SaY_iNH8Y5vk,5746
163
+ planar/workflows/test_lock_timeout.py,sha256=H78N090wJtiEg6SaJosfRWijpX6HwnyWyNNb7WaGPe0,5746
164
164
  planar/workflows/test_serialization.py,sha256=JfaveBRQTNMkucqkTorIMGcvi8S0j6uRtboFaWpCmes,39586
165
165
  planar/workflows/test_suspend_deserialization.py,sha256=ddw2jToSJ-ebQ0RfT7KWTRMCOs1nis1lprQiGIGuaJ0,7751
166
166
  planar/workflows/test_workflow.py,sha256=KArm9m44IBXKY9j4v_O74MAweFN6jEb7tVRomziaeFU,64011
167
167
  planar/workflows/tracing.py,sha256=E7E_kj2VBQisDqrllviIshbvOmB9QcEeRwMapunqio4,2732
168
168
  planar/workflows/wrappers.py,sha256=KON6RGg1D6yStboNbuMEeTXRpPTEa8S6Elh1tOnMAlM,1149
169
- planar-0.9.0.dist-info/METADATA,sha256=ak3k4S6dRs-2cuotMeuTZ2Egv0syC2rHER_zL706OFk,12199
170
- planar-0.9.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
171
- planar-0.9.0.dist-info/entry_points.txt,sha256=ZtFgrZ0eeoVmhLA51ESipK0nHg2t_prjW0Cm8WhpP54,95
172
- planar-0.9.0.dist-info/RECORD,,
169
+ planar-0.9.2.dist-info/WHEEL,sha256=Jb20R3Ili4n9P1fcwuLup21eQ5r9WXhs4_qy7VTrgPI,79
170
+ planar-0.9.2.dist-info/entry_points.txt,sha256=L3T0w9u2UPKWXv6JbXFWKU1d5xyEAq1xVWbpYS6mLNg,96
171
+ planar-0.9.2.dist-info/METADATA,sha256=nF_zOc5hfhs8BYdm83nO8Fp480qDGfCuTbJpx8Yf5FE,12313
172
+ planar-0.9.2.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.8.15
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -1,3 +1,4 @@
1
1
  [console_scripts]
2
2
  generate-llm-prompt = docs.generate_llm_prompt:main
3
3
  planar = planar.cli:main
4
+
planar/_version.py DELETED
@@ -1 +0,0 @@
1
- VERSION = "0.9.0"
@@ -1,4 +0,0 @@
1
- Wheel-Version: 1.0
2
- Generator: hatchling 1.27.0
3
- Root-Is-Purelib: true
4
- Tag: py3-none-any