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 +5 -1
- planar/ai/agent_utils.py +5 -7
- planar/ai/models.py +9 -3
- planar/ai/pydantic_ai.py +2 -2
- planar/app.py +5 -4
- planar/config.py +4 -1
- planar/data/dataset.py +26 -35
- planar/db/alembic/env.py +13 -6
- planar/db/alembic/script.py.mako +2 -2
- planar/db/db.py +39 -21
- planar/files/test_files.py +6 -7
- planar/modeling/mixins/test_auditable.py +2 -2
- planar/modeling/orm/planar_base_entity.py +4 -1
- planar/py.typed +0 -0
- planar/routers/test_agents_router.py +2 -2
- planar/routers/test_files_router.py +2 -2
- planar/routers/test_object_config_router.py +2 -2
- planar/routers/test_rule_router.py +2 -2
- planar/routers/test_workflow_router.py +6 -8
- planar/scaffold_templates/app/flows/process_invoice.py.j2 +1 -2
- planar/scaffold_templates/pyproject.toml.j2 +2 -2
- planar/test_sqlalchemy.py +36 -1
- planar/testing/fixtures.py +3 -17
- planar/workflows/test_lock_timeout.py +4 -4
- {planar-0.9.0.dist-info → planar-0.9.2.dist-info}/METADATA +34 -24
- {planar-0.9.0.dist-info → planar-0.9.2.dist-info}/RECORD +45 -45
- planar-0.9.2.dist-info/WHEEL +4 -0
- {planar-0.9.0.dist-info → planar-0.9.2.dist-info}/entry_points.txt +1 -0
- planar/_version.py +0 -1
- planar-0.9.0.dist-info/WHEEL +0 -4
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
|
-
|
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:
|
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
|
-
|
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:
|
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=
|
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(
|
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
|
-
|
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
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
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=
|
64
|
-
|
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 = {
|
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 =
|
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=
|
121
|
-
|
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.
|
planar/db/alembic/script.py.mako
CHANGED
@@ -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
|
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
|
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
|
193
|
-
#
|
194
|
-
execution_options={
|
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
|
-
|
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
|
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
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
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)
|
planar/files/test_files.py
CHANGED
@@ -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(
|
40
|
+
async def session(tmp_db_engine):
|
41
41
|
"""Create a database session."""
|
42
42
|
|
43
|
-
async with new_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
|
-
|
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(
|
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(
|
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(
|
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(
|
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=
|
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.",
|
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)
|
planar/testing/fixtures.py
CHANGED
@@ -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(
|
273
|
-
async with new_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(
|
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(
|
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(
|
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(
|
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(
|
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.
|
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
|
-
-
|
65
|
-
-
|
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
|
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=
|
22
|
-
planar/ai/agent_utils.py,sha256=
|
23
|
-
planar/ai/models.py,sha256=
|
24
|
-
planar/ai/pydantic_ai.py,sha256=
|
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=
|
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.
|
34
|
-
planar/db/
|
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=
|
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=
|
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=
|
89
|
-
planar/routers/test_files_router.py,sha256=
|
90
|
-
planar/routers/test_object_config_router.py,sha256=
|
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=
|
93
|
-
planar/routers/test_workflow_router.py,sha256=
|
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=
|
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=
|
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=
|
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.
|
170
|
-
planar-0.9.
|
171
|
-
planar-0.9.
|
172
|
-
planar-0.9.
|
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,,
|
planar/_version.py
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
VERSION = "0.9.0"
|
planar-0.9.0.dist-info/WHEEL
DELETED