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
File without changes
|
@@ -0,0 +1,11 @@
|
|
1
|
+
from planar.modeling.mixins import TimestampMixin
|
2
|
+
from planar.modeling.orm import Field, PlanarBaseEntity
|
3
|
+
|
4
|
+
|
5
|
+
class Invoice(PlanarBaseEntity, TimestampMixin, table=True):
|
6
|
+
"""Invoice entity"""
|
7
|
+
|
8
|
+
__tablename__ = "invoice"
|
9
|
+
|
10
|
+
vendor: str = Field()
|
11
|
+
amount: float = Field()
|
@@ -0,0 +1,67 @@
|
|
1
|
+
from planar.ai import Agent
|
2
|
+
from planar.ai.providers import OpenAI
|
3
|
+
from planar.files import PlanarFile
|
4
|
+
from planar.human import Human
|
5
|
+
from planar.rules.decorator import rule
|
6
|
+
from planar.workflows import step, workflow
|
7
|
+
from pydantic import BaseModel
|
8
|
+
|
9
|
+
|
10
|
+
class InvoiceData(BaseModel):
|
11
|
+
vendor: str
|
12
|
+
amount: float
|
13
|
+
|
14
|
+
|
15
|
+
class RuleInput(BaseModel):
|
16
|
+
amount: float
|
17
|
+
|
18
|
+
|
19
|
+
class RuleOutput(BaseModel):
|
20
|
+
approved: bool
|
21
|
+
reason: str
|
22
|
+
|
23
|
+
|
24
|
+
invoice_agent = Agent(
|
25
|
+
name="Invoice Agent",
|
26
|
+
model=OpenAI.gpt_4_1,
|
27
|
+
tools=[],
|
28
|
+
max_turns=1,
|
29
|
+
system_prompt="Extract vendor and amount from invoice text.",
|
30
|
+
user_prompt="{{ '{{input}}' }}",
|
31
|
+
input_type=PlanarFile,
|
32
|
+
output_type=InvoiceData,
|
33
|
+
)
|
34
|
+
|
35
|
+
|
36
|
+
human_review = Human(
|
37
|
+
name="Review Invoice",
|
38
|
+
title="Review Invoice",
|
39
|
+
input_type=InvoiceData,
|
40
|
+
output_type=InvoiceData,
|
41
|
+
)
|
42
|
+
|
43
|
+
|
44
|
+
@rule(description="Auto approve invoices under $1000")
|
45
|
+
def auto_approve(input: RuleInput) -> RuleOutput:
|
46
|
+
return RuleOutput(approved=input.amount < 1000, reason="Amount is under $1000")
|
47
|
+
|
48
|
+
|
49
|
+
@step(display_name="Extract invoice")
|
50
|
+
async def extract_invoice(invoice_file: PlanarFile) -> InvoiceData:
|
51
|
+
result = await invoice_agent(invoice_file)
|
52
|
+
return result.output
|
53
|
+
|
54
|
+
|
55
|
+
@step(display_name="Maybe approve")
|
56
|
+
async def maybe_approve(invoice: InvoiceData) -> InvoiceData:
|
57
|
+
auto_approve_result = await auto_approve(RuleInput(amount=invoice.amount))
|
58
|
+
if auto_approve_result.approved:
|
59
|
+
return invoice
|
60
|
+
reviewed_invoice = await human_review(invoice, suggested_data=invoice)
|
61
|
+
return reviewed_invoice.output
|
62
|
+
|
63
|
+
|
64
|
+
@workflow()
|
65
|
+
async def process_invoice(invoice_file: PlanarFile) -> InvoiceData:
|
66
|
+
invoice = await extract_invoice(invoice_file)
|
67
|
+
return await maybe_approve(invoice)
|
@@ -0,0 +1,13 @@
|
|
1
|
+
from planar import PlanarApp
|
2
|
+
|
3
|
+
from app.db.entities import Invoice
|
4
|
+
from app.flows.process_invoice import process_invoice
|
5
|
+
from app.flows.process_invoice import invoice_agent
|
6
|
+
|
7
|
+
|
8
|
+
app = (
|
9
|
+
PlanarApp(title="{{ name }}")
|
10
|
+
.register_entity(Invoice)
|
11
|
+
.register_workflow(process_invoice)
|
12
|
+
.register_agent(invoice_agent)
|
13
|
+
)
|
@@ -0,0 +1,34 @@
|
|
1
|
+
db_connections:
|
2
|
+
app:
|
3
|
+
driver: sqlite+aiosqlite
|
4
|
+
path: demo.db
|
5
|
+
|
6
|
+
app:
|
7
|
+
db_connection: app
|
8
|
+
|
9
|
+
storage:
|
10
|
+
backend: localdir
|
11
|
+
directory: .files
|
12
|
+
|
13
|
+
sse_hub: true
|
14
|
+
|
15
|
+
cors:
|
16
|
+
allow_origins: "^(?:https://(?:[a-zA-Z0-9-]+\\.)+coplane\\.(dev|com)|http://127.0.0.1:3000)$"
|
17
|
+
allow_credentials: true
|
18
|
+
allow_methods: ["*"]
|
19
|
+
allow_headers: ["*"]
|
20
|
+
|
21
|
+
ai_providers:
|
22
|
+
openai:
|
23
|
+
api_key: ${OPENAI_API_KEY}
|
24
|
+
|
25
|
+
logging:
|
26
|
+
planar:
|
27
|
+
level: INFO # enable INFO level logging for all modules in the "planar" package.
|
28
|
+
# Uncomment the following two lines to see SQL statements
|
29
|
+
# sqlalchemy.engine:
|
30
|
+
# level: INFO
|
31
|
+
# The root logger is represented by an empty string, so you can uncomment
|
32
|
+
# the following lines to enable INFO level for the whole application (except sqlalchemy.engine, which must be enabled above)
|
33
|
+
# "":
|
34
|
+
# level: INFO
|
@@ -0,0 +1,28 @@
|
|
1
|
+
db_connections:
|
2
|
+
app:
|
3
|
+
driver: postgresql+asyncpg
|
4
|
+
host: ${DB_HOST}
|
5
|
+
port: ${DB_PORT}
|
6
|
+
user: ${DB_USER}
|
7
|
+
password: ${DB_PASSWORD}
|
8
|
+
db: ${DB_NAME}
|
9
|
+
|
10
|
+
app:
|
11
|
+
db_connection: app
|
12
|
+
|
13
|
+
storage:
|
14
|
+
backend: s3
|
15
|
+
region: us-west-2
|
16
|
+
bucket_name: ${S3_BUCKET_NAME}
|
17
|
+
|
18
|
+
sse_hub: true
|
19
|
+
|
20
|
+
cors:
|
21
|
+
allow_origins: "^(?:https://(?:[a-zA-Z0-9-]+\\.)+coplane\\.(dev|com)|http://127.0.0.1:3000)$"
|
22
|
+
allow_credentials: true
|
23
|
+
allow_methods: ["*"]
|
24
|
+
allow_headers: ["*"]
|
25
|
+
|
26
|
+
ai_providers:
|
27
|
+
openai:
|
28
|
+
api_key: ${OPENAI_API_KEY}
|
Binary file
|
@@ -0,0 +1,148 @@
|
|
1
|
+
"""
|
2
|
+
Authentication context management for Planar.
|
3
|
+
|
4
|
+
This module provides context variables and utilities for managing the current
|
5
|
+
authenticated principal (user) throughout the request lifecycle.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from contextlib import contextmanager
|
9
|
+
from contextvars import ContextVar
|
10
|
+
from typing import Any, Iterator
|
11
|
+
|
12
|
+
from pydantic import BaseModel, Field
|
13
|
+
|
14
|
+
|
15
|
+
class Principal(BaseModel):
|
16
|
+
"""Represents an authenticated principal (user) with JWT claims."""
|
17
|
+
|
18
|
+
# Standard JWT claims
|
19
|
+
sub: str = Field(..., description="Subject (user ID)")
|
20
|
+
iss: str | None = Field(None, description="Issuer")
|
21
|
+
exp: int | None = Field(None, description="Expiration timestamp")
|
22
|
+
iat: int | None = Field(None, description="Issued at timestamp")
|
23
|
+
sid: str | None = Field(None, description="Session ID")
|
24
|
+
jti: str | None = Field(None, description="JWT ID")
|
25
|
+
|
26
|
+
# WorkOS specific claims
|
27
|
+
org_id: str | None = Field(None, description="Organization ID")
|
28
|
+
org_name: str | None = Field(None, description="Organization name")
|
29
|
+
user_first_name: str | None = Field(None, description="User's first name")
|
30
|
+
user_last_name: str | None = Field(None, description="User's last name")
|
31
|
+
user_email: str | None = Field(None, description="User's email address")
|
32
|
+
role: str | None = Field(None, description="User's role")
|
33
|
+
permissions: list[str] | None = Field(None, description="User's permissions")
|
34
|
+
|
35
|
+
# Additional custom claims
|
36
|
+
extra_claims: dict[str, Any] = Field(
|
37
|
+
default_factory=dict, description="Additional custom claims"
|
38
|
+
)
|
39
|
+
|
40
|
+
@classmethod
|
41
|
+
def from_jwt_payload(cls, payload: dict[str, Any]) -> "Principal":
|
42
|
+
"""Create a Principal from a JWT payload."""
|
43
|
+
if "sub" not in payload:
|
44
|
+
raise ValueError("JWT payload must contain 'sub' field")
|
45
|
+
|
46
|
+
standard_fields = {
|
47
|
+
"sub",
|
48
|
+
"iss",
|
49
|
+
"exp",
|
50
|
+
"iat",
|
51
|
+
"sid",
|
52
|
+
"jti",
|
53
|
+
"org_id",
|
54
|
+
"org_name",
|
55
|
+
"user_first_name",
|
56
|
+
"user_last_name",
|
57
|
+
"user_email",
|
58
|
+
"role",
|
59
|
+
"permissions",
|
60
|
+
}
|
61
|
+
|
62
|
+
# Extract standard fields
|
63
|
+
principal_data = {}
|
64
|
+
for field in standard_fields:
|
65
|
+
if field in payload:
|
66
|
+
principal_data[field] = payload[field]
|
67
|
+
|
68
|
+
# All other fields go into extra_claims
|
69
|
+
extra_claims = {k: v for k, v in payload.items() if k not in standard_fields}
|
70
|
+
principal_data["extra_claims"] = extra_claims
|
71
|
+
|
72
|
+
return cls(**principal_data)
|
73
|
+
|
74
|
+
|
75
|
+
# Context variable for the current principal
|
76
|
+
principal_var: ContextVar[Principal | None] = ContextVar("principal", default=None)
|
77
|
+
|
78
|
+
|
79
|
+
def get_current_principal() -> Principal | None:
|
80
|
+
"""
|
81
|
+
Get the current authenticated principal from context.
|
82
|
+
|
83
|
+
Returns:
|
84
|
+
The current Principal or None if not authenticated.
|
85
|
+
"""
|
86
|
+
return principal_var.get()
|
87
|
+
|
88
|
+
|
89
|
+
def require_principal() -> Principal:
|
90
|
+
"""
|
91
|
+
Get the current authenticated principal from context.
|
92
|
+
|
93
|
+
Returns:
|
94
|
+
The current Principal.
|
95
|
+
|
96
|
+
Raises:
|
97
|
+
RuntimeError: If no principal is set in context.
|
98
|
+
"""
|
99
|
+
principal = get_current_principal()
|
100
|
+
if principal is None:
|
101
|
+
raise RuntimeError("No authenticated principal in context")
|
102
|
+
return principal
|
103
|
+
|
104
|
+
|
105
|
+
def has_role(role: str) -> bool:
|
106
|
+
"""
|
107
|
+
Check if the current principal has the given role.
|
108
|
+
"""
|
109
|
+
principal = get_current_principal()
|
110
|
+
return principal is not None and principal.role == role
|
111
|
+
|
112
|
+
|
113
|
+
def set_principal(principal: Principal) -> Any:
|
114
|
+
"""
|
115
|
+
Set the current principal in context.
|
116
|
+
|
117
|
+
Args:
|
118
|
+
principal: The principal to set.
|
119
|
+
|
120
|
+
Returns:
|
121
|
+
A token that can be used to reset the context.
|
122
|
+
"""
|
123
|
+
return principal_var.set(principal)
|
124
|
+
|
125
|
+
|
126
|
+
def clear_principal(token: Any) -> None:
|
127
|
+
"""
|
128
|
+
Clear the current principal from context.
|
129
|
+
|
130
|
+
Args:
|
131
|
+
token: The token returned from set_principal.
|
132
|
+
"""
|
133
|
+
principal_var.reset(token)
|
134
|
+
|
135
|
+
|
136
|
+
@contextmanager
|
137
|
+
def as_principal(principal: Principal) -> Iterator[None]:
|
138
|
+
"""
|
139
|
+
Context manager that sets the current principal in context.
|
140
|
+
|
141
|
+
Args:
|
142
|
+
principal: The principal to set.
|
143
|
+
"""
|
144
|
+
token = set_principal(principal)
|
145
|
+
try:
|
146
|
+
yield
|
147
|
+
finally:
|
148
|
+
clear_principal(token)
|