planar 0.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- planar/.__init__.py.un~ +0 -0
- planar/._version.py.un~ +0 -0
- planar/.app.py.un~ +0 -0
- planar/.cli.py.un~ +0 -0
- planar/.config.py.un~ +0 -0
- planar/.context.py.un~ +0 -0
- planar/.db.py.un~ +0 -0
- planar/.di.py.un~ +0 -0
- planar/.engine.py.un~ +0 -0
- planar/.files.py.un~ +0 -0
- planar/.log_context.py.un~ +0 -0
- planar/.log_metadata.py.un~ +0 -0
- planar/.logging.py.un~ +0 -0
- planar/.object_registry.py.un~ +0 -0
- planar/.otel.py.un~ +0 -0
- planar/.server.py.un~ +0 -0
- planar/.session.py.un~ +0 -0
- planar/.sqlalchemy.py.un~ +0 -0
- planar/.task_local.py.un~ +0 -0
- planar/.test_app.py.un~ +0 -0
- planar/.test_config.py.un~ +0 -0
- planar/.test_object_config.py.un~ +0 -0
- planar/.test_sqlalchemy.py.un~ +0 -0
- planar/.test_utils.py.un~ +0 -0
- planar/.util.py.un~ +0 -0
- planar/.utils.py.un~ +0 -0
- planar/__init__.py +26 -0
- planar/_version.py +1 -0
- planar/ai/.__init__.py.un~ +0 -0
- planar/ai/._models.py.un~ +0 -0
- planar/ai/.agent.py.un~ +0 -0
- planar/ai/.agent_utils.py.un~ +0 -0
- planar/ai/.events.py.un~ +0 -0
- planar/ai/.files.py.un~ +0 -0
- planar/ai/.models.py.un~ +0 -0
- planar/ai/.providers.py.un~ +0 -0
- planar/ai/.pydantic_ai.py.un~ +0 -0
- planar/ai/.pydantic_ai_agent.py.un~ +0 -0
- planar/ai/.pydantic_ai_provider.py.un~ +0 -0
- planar/ai/.step.py.un~ +0 -0
- planar/ai/.test_agent.py.un~ +0 -0
- planar/ai/.test_agent_serialization.py.un~ +0 -0
- planar/ai/.test_providers.py.un~ +0 -0
- planar/ai/.utils.py.un~ +0 -0
- planar/ai/__init__.py +15 -0
- planar/ai/agent.py +457 -0
- planar/ai/agent_utils.py +205 -0
- planar/ai/models.py +140 -0
- planar/ai/providers.py +1088 -0
- planar/ai/test_agent.py +1298 -0
- planar/ai/test_agent_serialization.py +229 -0
- planar/ai/test_providers.py +463 -0
- planar/ai/utils.py +102 -0
- planar/app.py +494 -0
- planar/cli.py +282 -0
- planar/config.py +544 -0
- planar/db/.db.py.un~ +0 -0
- planar/db/__init__.py +17 -0
- planar/db/alembic/env.py +136 -0
- planar/db/alembic/script.py.mako +28 -0
- planar/db/alembic/versions/3476068c153c_initial_system_tables_migration.py +339 -0
- planar/db/alembic.ini +128 -0
- planar/db/db.py +318 -0
- planar/files/.config.py.un~ +0 -0
- planar/files/.local.py.un~ +0 -0
- planar/files/.local_filesystem.py.un~ +0 -0
- planar/files/.model.py.un~ +0 -0
- planar/files/.models.py.un~ +0 -0
- planar/files/.s3.py.un~ +0 -0
- planar/files/.storage.py.un~ +0 -0
- planar/files/.test_files.py.un~ +0 -0
- planar/files/__init__.py +2 -0
- planar/files/models.py +162 -0
- planar/files/storage/.__init__.py.un~ +0 -0
- planar/files/storage/.base.py.un~ +0 -0
- planar/files/storage/.config.py.un~ +0 -0
- planar/files/storage/.context.py.un~ +0 -0
- planar/files/storage/.local_directory.py.un~ +0 -0
- planar/files/storage/.test_local_directory.py.un~ +0 -0
- planar/files/storage/.test_s3.py.un~ +0 -0
- planar/files/storage/base.py +61 -0
- planar/files/storage/config.py +44 -0
- planar/files/storage/context.py +15 -0
- planar/files/storage/local_directory.py +188 -0
- planar/files/storage/s3.py +220 -0
- planar/files/storage/test_local_directory.py +162 -0
- planar/files/storage/test_s3.py +299 -0
- planar/files/test_files.py +283 -0
- planar/human/.human.py.un~ +0 -0
- planar/human/.test_human.py.un~ +0 -0
- planar/human/__init__.py +2 -0
- planar/human/human.py +458 -0
- planar/human/models.py +80 -0
- planar/human/test_human.py +385 -0
- planar/logging/.__init__.py.un~ +0 -0
- planar/logging/.attributes.py.un~ +0 -0
- planar/logging/.formatter.py.un~ +0 -0
- planar/logging/.logger.py.un~ +0 -0
- planar/logging/.otel.py.un~ +0 -0
- planar/logging/.tracer.py.un~ +0 -0
- planar/logging/__init__.py +10 -0
- planar/logging/attributes.py +54 -0
- planar/logging/context.py +14 -0
- planar/logging/formatter.py +113 -0
- planar/logging/logger.py +114 -0
- planar/logging/otel.py +51 -0
- planar/modeling/.mixin.py.un~ +0 -0
- planar/modeling/.storage.py.un~ +0 -0
- planar/modeling/__init__.py +0 -0
- planar/modeling/field_helpers.py +59 -0
- planar/modeling/json_schema_generator.py +94 -0
- planar/modeling/mixins/__init__.py +10 -0
- planar/modeling/mixins/auditable.py +52 -0
- planar/modeling/mixins/test_auditable.py +97 -0
- planar/modeling/mixins/test_timestamp.py +134 -0
- planar/modeling/mixins/test_uuid_primary_key.py +52 -0
- planar/modeling/mixins/timestamp.py +53 -0
- planar/modeling/mixins/uuid_primary_key.py +19 -0
- planar/modeling/orm/.planar_base_model.py.un~ +0 -0
- planar/modeling/orm/__init__.py +18 -0
- planar/modeling/orm/planar_base_entity.py +29 -0
- planar/modeling/orm/query_filter_builder.py +122 -0
- planar/modeling/orm/reexports.py +15 -0
- planar/object_config/.object_config.py.un~ +0 -0
- planar/object_config/__init__.py +11 -0
- planar/object_config/models.py +114 -0
- planar/object_config/object_config.py +378 -0
- planar/object_registry.py +100 -0
- planar/registry_items.py +65 -0
- planar/routers/.__init__.py.un~ +0 -0
- planar/routers/.agents_router.py.un~ +0 -0
- planar/routers/.crud.py.un~ +0 -0
- planar/routers/.decision.py.un~ +0 -0
- planar/routers/.event.py.un~ +0 -0
- planar/routers/.file_attachment.py.un~ +0 -0
- planar/routers/.files.py.un~ +0 -0
- planar/routers/.files_router.py.un~ +0 -0
- planar/routers/.human.py.un~ +0 -0
- planar/routers/.info.py.un~ +0 -0
- planar/routers/.models.py.un~ +0 -0
- planar/routers/.object_config_router.py.un~ +0 -0
- planar/routers/.rule.py.un~ +0 -0
- planar/routers/.test_object_config_router.py.un~ +0 -0
- planar/routers/.test_workflow_router.py.un~ +0 -0
- planar/routers/.workflow.py.un~ +0 -0
- planar/routers/__init__.py +13 -0
- planar/routers/agents_router.py +197 -0
- planar/routers/entity_router.py +143 -0
- planar/routers/event.py +91 -0
- planar/routers/files.py +142 -0
- planar/routers/human.py +151 -0
- planar/routers/info.py +131 -0
- planar/routers/models.py +170 -0
- planar/routers/object_config_router.py +133 -0
- planar/routers/rule.py +108 -0
- planar/routers/test_agents_router.py +174 -0
- planar/routers/test_object_config_router.py +367 -0
- planar/routers/test_routes_security.py +169 -0
- planar/routers/test_rule_router.py +470 -0
- planar/routers/test_workflow_router.py +274 -0
- planar/routers/workflow.py +468 -0
- planar/rules/.decorator.py.un~ +0 -0
- planar/rules/.runner.py.un~ +0 -0
- planar/rules/.test_rules.py.un~ +0 -0
- planar/rules/__init__.py +23 -0
- planar/rules/decorator.py +184 -0
- planar/rules/models.py +355 -0
- planar/rules/rule_configuration.py +191 -0
- planar/rules/runner.py +64 -0
- planar/rules/test_rules.py +750 -0
- planar/scaffold_templates/app/__init__.py.j2 +0 -0
- planar/scaffold_templates/app/db/entities.py.j2 +11 -0
- planar/scaffold_templates/app/flows/process_invoice.py.j2 +67 -0
- planar/scaffold_templates/main.py.j2 +13 -0
- planar/scaffold_templates/planar.dev.yaml.j2 +34 -0
- planar/scaffold_templates/planar.prod.yaml.j2 +28 -0
- planar/scaffold_templates/pyproject.toml.j2 +10 -0
- planar/security/.jwt_middleware.py.un~ +0 -0
- planar/security/auth_context.py +148 -0
- planar/security/authorization.py +388 -0
- planar/security/default_policies.cedar +77 -0
- planar/security/jwt_middleware.py +116 -0
- planar/security/security_context.py +18 -0
- planar/security/tests/test_authorization_context.py +78 -0
- planar/security/tests/test_cedar_basics.py +41 -0
- planar/security/tests/test_cedar_policies.py +158 -0
- planar/security/tests/test_jwt_principal_context.py +179 -0
- planar/session.py +40 -0
- planar/sse/.constants.py.un~ +0 -0
- planar/sse/.example.html.un~ +0 -0
- planar/sse/.hub.py.un~ +0 -0
- planar/sse/.model.py.un~ +0 -0
- planar/sse/.proxy.py.un~ +0 -0
- planar/sse/constants.py +1 -0
- planar/sse/example.html +126 -0
- planar/sse/hub.py +216 -0
- planar/sse/model.py +8 -0
- planar/sse/proxy.py +257 -0
- planar/task_local.py +37 -0
- planar/test_app.py +51 -0
- planar/test_cli.py +372 -0
- planar/test_config.py +512 -0
- planar/test_object_config.py +527 -0
- planar/test_object_registry.py +14 -0
- planar/test_sqlalchemy.py +158 -0
- planar/test_utils.py +105 -0
- planar/testing/.client.py.un~ +0 -0
- planar/testing/.memory_storage.py.un~ +0 -0
- planar/testing/.planar_test_client.py.un~ +0 -0
- planar/testing/.predictable_tracer.py.un~ +0 -0
- planar/testing/.synchronizable_tracer.py.un~ +0 -0
- planar/testing/.test_memory_storage.py.un~ +0 -0
- planar/testing/.workflow_observer.py.un~ +0 -0
- planar/testing/__init__.py +0 -0
- planar/testing/memory_storage.py +78 -0
- planar/testing/planar_test_client.py +54 -0
- planar/testing/synchronizable_tracer.py +153 -0
- planar/testing/test_memory_storage.py +143 -0
- planar/testing/workflow_observer.py +73 -0
- planar/utils.py +70 -0
- planar/workflows/.__init__.py.un~ +0 -0
- planar/workflows/.builtin_steps.py.un~ +0 -0
- planar/workflows/.concurrency_tracing.py.un~ +0 -0
- planar/workflows/.context.py.un~ +0 -0
- planar/workflows/.contrib.py.un~ +0 -0
- planar/workflows/.decorators.py.un~ +0 -0
- planar/workflows/.durable_test.py.un~ +0 -0
- planar/workflows/.errors.py.un~ +0 -0
- planar/workflows/.events.py.un~ +0 -0
- planar/workflows/.exceptions.py.un~ +0 -0
- planar/workflows/.execution.py.un~ +0 -0
- planar/workflows/.human.py.un~ +0 -0
- planar/workflows/.lock.py.un~ +0 -0
- planar/workflows/.misc.py.un~ +0 -0
- planar/workflows/.model.py.un~ +0 -0
- planar/workflows/.models.py.un~ +0 -0
- planar/workflows/.notifications.py.un~ +0 -0
- planar/workflows/.orchestrator.py.un~ +0 -0
- planar/workflows/.runtime.py.un~ +0 -0
- planar/workflows/.serialization.py.un~ +0 -0
- planar/workflows/.step.py.un~ +0 -0
- planar/workflows/.step_core.py.un~ +0 -0
- planar/workflows/.sub_workflow_runner.py.un~ +0 -0
- planar/workflows/.sub_workflow_scheduler.py.un~ +0 -0
- planar/workflows/.test_concurrency.py.un~ +0 -0
- planar/workflows/.test_concurrency_detection.py.un~ +0 -0
- planar/workflows/.test_human.py.un~ +0 -0
- planar/workflows/.test_lock_timeout.py.un~ +0 -0
- planar/workflows/.test_orchestrator.py.un~ +0 -0
- planar/workflows/.test_race_conditions.py.un~ +0 -0
- planar/workflows/.test_serialization.py.un~ +0 -0
- planar/workflows/.test_suspend_deserialization.py.un~ +0 -0
- planar/workflows/.test_workflow.py.un~ +0 -0
- planar/workflows/.tracing.py.un~ +0 -0
- planar/workflows/.types.py.un~ +0 -0
- planar/workflows/.util.py.un~ +0 -0
- planar/workflows/.utils.py.un~ +0 -0
- planar/workflows/.workflow.py.un~ +0 -0
- planar/workflows/.workflow_wrapper.py.un~ +0 -0
- planar/workflows/.wrappers.py.un~ +0 -0
- planar/workflows/__init__.py +42 -0
- planar/workflows/context.py +44 -0
- planar/workflows/contrib.py +190 -0
- planar/workflows/decorators.py +217 -0
- planar/workflows/events.py +185 -0
- planar/workflows/exceptions.py +34 -0
- planar/workflows/execution.py +198 -0
- planar/workflows/lock.py +229 -0
- planar/workflows/misc.py +5 -0
- planar/workflows/models.py +154 -0
- planar/workflows/notifications.py +96 -0
- planar/workflows/orchestrator.py +383 -0
- planar/workflows/query.py +256 -0
- planar/workflows/serialization.py +409 -0
- planar/workflows/step_core.py +373 -0
- planar/workflows/step_metadata.py +357 -0
- planar/workflows/step_testing_utils.py +86 -0
- planar/workflows/sub_workflow_runner.py +191 -0
- planar/workflows/test_concurrency_detection.py +120 -0
- planar/workflows/test_lock_timeout.py +140 -0
- planar/workflows/test_serialization.py +1195 -0
- planar/workflows/test_suspend_deserialization.py +231 -0
- planar/workflows/test_workflow.py +1967 -0
- planar/workflows/tracing.py +106 -0
- planar/workflows/wrappers.py +41 -0
- planar-0.5.0.dist-info/METADATA +285 -0
- planar-0.5.0.dist-info/RECORD +289 -0
- planar-0.5.0.dist-info/WHEEL +4 -0
- planar-0.5.0.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,53 @@
|
|
1
|
+
from datetime import datetime
|
2
|
+
from typing import Any, Callable
|
3
|
+
|
4
|
+
from sqlmodel import Field, SQLModel
|
5
|
+
|
6
|
+
from planar.utils import utc_now
|
7
|
+
|
8
|
+
|
9
|
+
def timestamp_column(
|
10
|
+
index: bool = False,
|
11
|
+
nullable: bool = False,
|
12
|
+
onupdate: Callable[[], datetime] | bool | None = None,
|
13
|
+
default: Callable[[], datetime] | None = utc_now,
|
14
|
+
):
|
15
|
+
if onupdate is True:
|
16
|
+
onupdate = utc_now
|
17
|
+
return Field(
|
18
|
+
default_factory=default,
|
19
|
+
nullable=nullable,
|
20
|
+
index=index,
|
21
|
+
sa_column_kwargs={"onupdate": onupdate},
|
22
|
+
)
|
23
|
+
|
24
|
+
|
25
|
+
class TimestampMixin(SQLModel, table=False):
|
26
|
+
"""
|
27
|
+
Mixin that adds created_at and updated_at fields to a model.
|
28
|
+
|
29
|
+
This standardizes timestamp handling across all internal models.
|
30
|
+
|
31
|
+
Attributes:
|
32
|
+
created_at: Timestamp when the record was created
|
33
|
+
updated_at: Timestamp that updates whenever the record is modified
|
34
|
+
"""
|
35
|
+
|
36
|
+
__abstract__ = True
|
37
|
+
|
38
|
+
created_at: datetime = timestamp_column()
|
39
|
+
updated_at: datetime = timestamp_column(onupdate=utc_now)
|
40
|
+
|
41
|
+
def __init__(self, **kwargs: Any):
|
42
|
+
"""
|
43
|
+
Initializes the TimestampMixin.
|
44
|
+
Ensures that `updated_at` is the same as `created_at` if `updated_at`
|
45
|
+
is not explicitly provided during instantiation.
|
46
|
+
"""
|
47
|
+
super().__init__(**kwargs)
|
48
|
+
# If 'updated_at' was not passed during construction,
|
49
|
+
# set it to the value of 'created_at'.
|
50
|
+
# 'created_at' itself would have been set by super().__init__()
|
51
|
+
# either from kwargs or its default_factory.
|
52
|
+
if "updated_at" not in kwargs and self.created_at is not None:
|
53
|
+
self.updated_at = self.created_at
|
@@ -0,0 +1,19 @@
|
|
1
|
+
from uuid import UUID, uuid4
|
2
|
+
|
3
|
+
from sqlmodel import Field, SQLModel
|
4
|
+
|
5
|
+
|
6
|
+
class UUIDPrimaryKeyMixin(SQLModel, table=False):
|
7
|
+
"""
|
8
|
+
Mixin that provides a UUID primary key field.
|
9
|
+
|
10
|
+
This standardizes primary key handling across all models that need
|
11
|
+
a UUID-based primary key.
|
12
|
+
|
13
|
+
Attributes:
|
14
|
+
id: UUID primary key field with automatic generation
|
15
|
+
"""
|
16
|
+
|
17
|
+
__abstract__ = True
|
18
|
+
|
19
|
+
id: UUID = Field(default_factory=uuid4, primary_key=True)
|
Binary file
|
@@ -0,0 +1,18 @@
|
|
1
|
+
from .planar_base_entity import PLANAR_APPLICATION_METADATA, PlanarBaseEntity
|
2
|
+
from .reexports import (
|
3
|
+
Field,
|
4
|
+
Relationship,
|
5
|
+
Session,
|
6
|
+
SQLModel,
|
7
|
+
create_engine,
|
8
|
+
)
|
9
|
+
|
10
|
+
__all__ = [
|
11
|
+
"Field",
|
12
|
+
"Relationship",
|
13
|
+
"Session",
|
14
|
+
"SQLModel",
|
15
|
+
"create_engine",
|
16
|
+
"PlanarBaseEntity",
|
17
|
+
"PLANAR_APPLICATION_METADATA",
|
18
|
+
]
|
@@ -0,0 +1,29 @@
|
|
1
|
+
from pydantic import ConfigDict
|
2
|
+
from sqlalchemy import MetaData, event
|
3
|
+
from sqlalchemy.engine import Connection
|
4
|
+
from sqlalchemy.orm import Mapper
|
5
|
+
|
6
|
+
from planar.logging import get_logger
|
7
|
+
from planar.modeling.mixins.auditable import AuditableMixin
|
8
|
+
from planar.modeling.mixins.uuid_primary_key import UUIDPrimaryKeyMixin
|
9
|
+
|
10
|
+
from .reexports import SQLModel
|
11
|
+
|
12
|
+
logger = get_logger("orm.PlanarBaseEntity")
|
13
|
+
|
14
|
+
|
15
|
+
PLANAR_APPLICATION_METADATA = MetaData()
|
16
|
+
|
17
|
+
|
18
|
+
class PlanarBaseEntity(UUIDPrimaryKeyMixin, AuditableMixin, SQLModel, table=False):
|
19
|
+
__abstract__ = True
|
20
|
+
model_config = ConfigDict(validate_assignment=True) # type: ignore
|
21
|
+
metadata = PLANAR_APPLICATION_METADATA
|
22
|
+
|
23
|
+
|
24
|
+
@event.listens_for(PlanarBaseEntity, "before_delete", propagate=True)
|
25
|
+
def log_deletion(
|
26
|
+
mapper: Mapper, connection: Connection, target: PlanarBaseEntity
|
27
|
+
) -> None:
|
28
|
+
"""Logs the deletion of the entity."""
|
29
|
+
logger.info("deleting entity", table_name=target.__tablename__, key=target.id)
|
@@ -0,0 +1,122 @@
|
|
1
|
+
from sqlalchemy.sql import func as sql_func
|
2
|
+
from sqlmodel import desc, select
|
3
|
+
|
4
|
+
from planar.routers.models import SortDirection
|
5
|
+
|
6
|
+
|
7
|
+
def build_paginated_query(
|
8
|
+
query, filters=None, offset=None, limit=None, order_by=None, order_direction=None
|
9
|
+
):
|
10
|
+
"""
|
11
|
+
Helper function to build paginated and filtered queries.
|
12
|
+
|
13
|
+
Args:
|
14
|
+
query: The base SQL query to build upon
|
15
|
+
filters: Optional list of filter conditions, where each condition is a tuple of
|
16
|
+
(column, operator, value). Operator should be one of:
|
17
|
+
'==', '!=', '>', '>=', '<', '<=', 'like', 'ilike', 'in', 'not_in'
|
18
|
+
For example: [(User.name, '==', 'John'), (User.age, '>', 18)]
|
19
|
+
For date ranges: [(Workflow.created_at, '>=', start_date)]
|
20
|
+
offset: Optional offset for pagination
|
21
|
+
limit: Optional limit for pagination
|
22
|
+
order_by: Optional field or list of fields to order by
|
23
|
+
order_direction: Optional direction to order by
|
24
|
+
Returns:
|
25
|
+
Tuple of (paginated query, total count query)
|
26
|
+
The count query is guaranteed to work with the session.exec().one() pattern
|
27
|
+
"""
|
28
|
+
# Create a copy of the query for filtering
|
29
|
+
filtered_query = query
|
30
|
+
|
31
|
+
# Apply filters if provided
|
32
|
+
if filters:
|
33
|
+
for column, operator, value in filters:
|
34
|
+
if value is not None: # Skip None values
|
35
|
+
if operator == "==" or operator == "=":
|
36
|
+
filtered_query = filtered_query.where(column == value)
|
37
|
+
elif operator == "!=":
|
38
|
+
filtered_query = filtered_query.where(column != value)
|
39
|
+
elif operator == ">":
|
40
|
+
filtered_query = filtered_query.where(column > value)
|
41
|
+
elif operator == ">=":
|
42
|
+
filtered_query = filtered_query.where(column >= value)
|
43
|
+
elif operator == "<":
|
44
|
+
filtered_query = filtered_query.where(column < value)
|
45
|
+
elif operator == "<=":
|
46
|
+
filtered_query = filtered_query.where(column <= value)
|
47
|
+
elif operator == "like":
|
48
|
+
filtered_query = filtered_query.where(column.like(value))
|
49
|
+
elif operator == "ilike":
|
50
|
+
filtered_query = filtered_query.where(column.ilike(value))
|
51
|
+
elif operator == "in":
|
52
|
+
filtered_query = filtered_query.where(column.in_(value))
|
53
|
+
elif operator == "not_in":
|
54
|
+
filtered_query = filtered_query.where(column.not_in(value))
|
55
|
+
|
56
|
+
# Create a total count query based on the filtered query
|
57
|
+
# For select queries with a clear table source
|
58
|
+
if hasattr(filtered_query, "whereclause"):
|
59
|
+
# For standard select queries, create a count query from the same structure
|
60
|
+
count_query = select(sql_func.count())
|
61
|
+
|
62
|
+
# Try to determine the table to select from
|
63
|
+
if (
|
64
|
+
hasattr(filtered_query, "get_final_froms")
|
65
|
+
and filtered_query.get_final_froms()
|
66
|
+
):
|
67
|
+
count_query = count_query.select_from(filtered_query.get_final_froms()[0])
|
68
|
+
elif hasattr(filtered_query, "columns") and filtered_query.columns:
|
69
|
+
# If we can't get froms, try to extract from columns
|
70
|
+
for col in filtered_query.columns:
|
71
|
+
if hasattr(col, "table"):
|
72
|
+
count_query = count_query.select_from(col.table)
|
73
|
+
break
|
74
|
+
|
75
|
+
# Apply the same where clause if it exists
|
76
|
+
if filtered_query.whereclause is not None:
|
77
|
+
count_query = count_query.where(filtered_query.whereclause)
|
78
|
+
else:
|
79
|
+
# For manually constructed queries, extract from first part of query
|
80
|
+
# Assuming the query has a clear identifier of the table it's querying
|
81
|
+
first_table = None
|
82
|
+
|
83
|
+
# Try to find the table by examining the query structure
|
84
|
+
# This is a simplified approach - adjust as needed based on your query structure
|
85
|
+
if hasattr(query, "columns") and query.columns:
|
86
|
+
for col in query.columns:
|
87
|
+
if hasattr(col, "table"):
|
88
|
+
first_table = col.table
|
89
|
+
break
|
90
|
+
|
91
|
+
# Build count query based on the table and where clause
|
92
|
+
if first_table is not None:
|
93
|
+
count_query = select(sql_func.count()).select_from(first_table)
|
94
|
+
if (
|
95
|
+
hasattr(filtered_query, "whereclause")
|
96
|
+
and filtered_query.whereclause is not None
|
97
|
+
):
|
98
|
+
count_query = count_query.where(filtered_query.whereclause)
|
99
|
+
else:
|
100
|
+
# If we can't determine the table, create a dummy count query that returns 0
|
101
|
+
# This ensures we don't need fallback code in the endpoint handlers
|
102
|
+
count_query = select(sql_func.lit(0).label("count"))
|
103
|
+
|
104
|
+
# Apply pagination
|
105
|
+
result_query = filtered_query
|
106
|
+
|
107
|
+
# Apply offset if provided
|
108
|
+
if offset is not None:
|
109
|
+
result_query = result_query.offset(offset)
|
110
|
+
|
111
|
+
# Apply limit if provided
|
112
|
+
if limit is not None:
|
113
|
+
result_query = result_query.limit(limit)
|
114
|
+
|
115
|
+
# Apply ordering
|
116
|
+
if order_by:
|
117
|
+
if order_direction == SortDirection.ASC:
|
118
|
+
result_query = result_query.order_by(order_by)
|
119
|
+
else:
|
120
|
+
result_query = result_query.order_by(desc(order_by))
|
121
|
+
|
122
|
+
return result_query, count_query
|
@@ -0,0 +1,15 @@
|
|
1
|
+
import sqlmodel as _sqlmodel
|
2
|
+
|
3
|
+
SQLModel = _sqlmodel.SQLModel
|
4
|
+
Field = _sqlmodel.Field
|
5
|
+
Relationship = _sqlmodel.Relationship
|
6
|
+
Session = _sqlmodel.Session
|
7
|
+
create_engine = _sqlmodel.create_engine
|
8
|
+
|
9
|
+
__all__ = [
|
10
|
+
"SQLModel",
|
11
|
+
"Field",
|
12
|
+
"Relationship",
|
13
|
+
"Session",
|
14
|
+
"create_engine",
|
15
|
+
]
|
Binary file
|
@@ -0,0 +1,11 @@
|
|
1
|
+
from .models import ConfigurableObjectType, ObjectConfiguration, ObjectConfigurationBase
|
2
|
+
from .object_config import DEFAULT_UUID, ConfigNotFoundError, ObjectConfigurationIO
|
3
|
+
|
4
|
+
__all__ = [
|
5
|
+
"ObjectConfiguration",
|
6
|
+
"ObjectConfigurationBase",
|
7
|
+
"ConfigurableObjectType",
|
8
|
+
"DEFAULT_UUID",
|
9
|
+
"ObjectConfigurationIO",
|
10
|
+
"ConfigNotFoundError",
|
11
|
+
]
|
@@ -0,0 +1,114 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import json
|
4
|
+
from datetime import datetime
|
5
|
+
from enum import Enum
|
6
|
+
from typing import Any, Generic, TypeVar
|
7
|
+
from uuid import UUID, uuid4
|
8
|
+
|
9
|
+
from pydantic import BaseModel
|
10
|
+
from sqlalchemy import VARCHAR, UniqueConstraint
|
11
|
+
from sqlalchemy.sql.type_api import TypeDecorator
|
12
|
+
from sqlmodel import Column
|
13
|
+
from sqlmodel import Field as SQLField
|
14
|
+
|
15
|
+
from planar.db import PlanarInternalBase
|
16
|
+
from planar.modeling.mixins.timestamp import timestamp_column
|
17
|
+
|
18
|
+
T = TypeVar("T", bound="BaseModel")
|
19
|
+
V = TypeVar("V")
|
20
|
+
|
21
|
+
|
22
|
+
class ConfigurableObjectType(str, Enum):
|
23
|
+
RULE = "rule"
|
24
|
+
AGENT = "agent"
|
25
|
+
|
26
|
+
|
27
|
+
class JSONEncodedDict(TypeDecorator):
|
28
|
+
"""Create a SQLAlchemy type that converts BaseModel to JSON string on write and to dict on read."""
|
29
|
+
|
30
|
+
impl = VARCHAR
|
31
|
+
cache_ok = True # Required for SQLAlchemy caching mechanism
|
32
|
+
|
33
|
+
def process_bind_param(self, value, dialect):
|
34
|
+
if value is None:
|
35
|
+
return None
|
36
|
+
|
37
|
+
if isinstance(value, BaseModel):
|
38
|
+
return value.model_dump_json(by_alias=True)
|
39
|
+
|
40
|
+
raise ValueError(f"Invalid type: {type(value)}")
|
41
|
+
|
42
|
+
def process_result_value(self, value, dialect):
|
43
|
+
if value is None:
|
44
|
+
return None
|
45
|
+
|
46
|
+
return json.loads(value)
|
47
|
+
|
48
|
+
|
49
|
+
class DiffErrorCode(Enum):
|
50
|
+
"""Error codes for dictionary comparison diagnostics."""
|
51
|
+
|
52
|
+
MISSING_FIELD = "MISSING_FIELD"
|
53
|
+
VALUE_MISMATCH = "VALUE_MISMATCH"
|
54
|
+
EXTRA_FIELD = "EXTRA_FIELD"
|
55
|
+
CONFIG_MODEL_CHANGED = "CONFIG_MODEL_CHANGED"
|
56
|
+
|
57
|
+
|
58
|
+
class ConfigDiagnosticIssue(BaseModel):
|
59
|
+
"""Represents a single diagnostic issue found during dictionary comparison."""
|
60
|
+
|
61
|
+
error_code: DiffErrorCode
|
62
|
+
field_path: str
|
63
|
+
message: str
|
64
|
+
reference_value: Any | None = None
|
65
|
+
current_value: Any | None = None
|
66
|
+
for_object: str
|
67
|
+
|
68
|
+
|
69
|
+
class ConfigDiagnostics(BaseModel, Generic[T]):
|
70
|
+
is_valid: bool
|
71
|
+
suggested_fix: T | None = None
|
72
|
+
issues: list[ConfigDiagnosticIssue]
|
73
|
+
|
74
|
+
|
75
|
+
class ObjectConfigurationBase(BaseModel, Generic[T]):
|
76
|
+
"""Base Pydantic model for object configurations without SQLModel dependencies.
|
77
|
+
|
78
|
+
This class mirrors the fields in ObjectConfiguration but can be used for
|
79
|
+
serialization in FastAPI routes and other places where SQLModel references
|
80
|
+
should be avoided.
|
81
|
+
"""
|
82
|
+
|
83
|
+
id: UUID
|
84
|
+
object_name: str
|
85
|
+
object_type: ConfigurableObjectType
|
86
|
+
created_at: datetime
|
87
|
+
version: int
|
88
|
+
data: T
|
89
|
+
active: bool
|
90
|
+
|
91
|
+
|
92
|
+
class ObjectConfiguration(PlanarInternalBase, Generic[T], table=True):
|
93
|
+
__table_args__ = (
|
94
|
+
UniqueConstraint(
|
95
|
+
"object_name",
|
96
|
+
"object_type",
|
97
|
+
"version",
|
98
|
+
name="uq_object_config_name_type_version",
|
99
|
+
),
|
100
|
+
)
|
101
|
+
|
102
|
+
__tablename__ = "object_configuration" # type: ignore
|
103
|
+
|
104
|
+
id: UUID = SQLField(default_factory=uuid4, primary_key=True)
|
105
|
+
|
106
|
+
object_name: str = SQLField(index=True)
|
107
|
+
object_type: ConfigurableObjectType = SQLField(index=True)
|
108
|
+
created_at: datetime = timestamp_column()
|
109
|
+
|
110
|
+
version: int = SQLField(default=1)
|
111
|
+
|
112
|
+
data: T = SQLField(sa_column=Column(JSONEncodedDict))
|
113
|
+
|
114
|
+
active: bool = SQLField(default=False)
|