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.
Files changed (289) hide show
  1. planar/.__init__.py.un~ +0 -0
  2. planar/._version.py.un~ +0 -0
  3. planar/.app.py.un~ +0 -0
  4. planar/.cli.py.un~ +0 -0
  5. planar/.config.py.un~ +0 -0
  6. planar/.context.py.un~ +0 -0
  7. planar/.db.py.un~ +0 -0
  8. planar/.di.py.un~ +0 -0
  9. planar/.engine.py.un~ +0 -0
  10. planar/.files.py.un~ +0 -0
  11. planar/.log_context.py.un~ +0 -0
  12. planar/.log_metadata.py.un~ +0 -0
  13. planar/.logging.py.un~ +0 -0
  14. planar/.object_registry.py.un~ +0 -0
  15. planar/.otel.py.un~ +0 -0
  16. planar/.server.py.un~ +0 -0
  17. planar/.session.py.un~ +0 -0
  18. planar/.sqlalchemy.py.un~ +0 -0
  19. planar/.task_local.py.un~ +0 -0
  20. planar/.test_app.py.un~ +0 -0
  21. planar/.test_config.py.un~ +0 -0
  22. planar/.test_object_config.py.un~ +0 -0
  23. planar/.test_sqlalchemy.py.un~ +0 -0
  24. planar/.test_utils.py.un~ +0 -0
  25. planar/.util.py.un~ +0 -0
  26. planar/.utils.py.un~ +0 -0
  27. planar/__init__.py +26 -0
  28. planar/_version.py +1 -0
  29. planar/ai/.__init__.py.un~ +0 -0
  30. planar/ai/._models.py.un~ +0 -0
  31. planar/ai/.agent.py.un~ +0 -0
  32. planar/ai/.agent_utils.py.un~ +0 -0
  33. planar/ai/.events.py.un~ +0 -0
  34. planar/ai/.files.py.un~ +0 -0
  35. planar/ai/.models.py.un~ +0 -0
  36. planar/ai/.providers.py.un~ +0 -0
  37. planar/ai/.pydantic_ai.py.un~ +0 -0
  38. planar/ai/.pydantic_ai_agent.py.un~ +0 -0
  39. planar/ai/.pydantic_ai_provider.py.un~ +0 -0
  40. planar/ai/.step.py.un~ +0 -0
  41. planar/ai/.test_agent.py.un~ +0 -0
  42. planar/ai/.test_agent_serialization.py.un~ +0 -0
  43. planar/ai/.test_providers.py.un~ +0 -0
  44. planar/ai/.utils.py.un~ +0 -0
  45. planar/ai/__init__.py +15 -0
  46. planar/ai/agent.py +457 -0
  47. planar/ai/agent_utils.py +205 -0
  48. planar/ai/models.py +140 -0
  49. planar/ai/providers.py +1088 -0
  50. planar/ai/test_agent.py +1298 -0
  51. planar/ai/test_agent_serialization.py +229 -0
  52. planar/ai/test_providers.py +463 -0
  53. planar/ai/utils.py +102 -0
  54. planar/app.py +494 -0
  55. planar/cli.py +282 -0
  56. planar/config.py +544 -0
  57. planar/db/.db.py.un~ +0 -0
  58. planar/db/__init__.py +17 -0
  59. planar/db/alembic/env.py +136 -0
  60. planar/db/alembic/script.py.mako +28 -0
  61. planar/db/alembic/versions/3476068c153c_initial_system_tables_migration.py +339 -0
  62. planar/db/alembic.ini +128 -0
  63. planar/db/db.py +318 -0
  64. planar/files/.config.py.un~ +0 -0
  65. planar/files/.local.py.un~ +0 -0
  66. planar/files/.local_filesystem.py.un~ +0 -0
  67. planar/files/.model.py.un~ +0 -0
  68. planar/files/.models.py.un~ +0 -0
  69. planar/files/.s3.py.un~ +0 -0
  70. planar/files/.storage.py.un~ +0 -0
  71. planar/files/.test_files.py.un~ +0 -0
  72. planar/files/__init__.py +2 -0
  73. planar/files/models.py +162 -0
  74. planar/files/storage/.__init__.py.un~ +0 -0
  75. planar/files/storage/.base.py.un~ +0 -0
  76. planar/files/storage/.config.py.un~ +0 -0
  77. planar/files/storage/.context.py.un~ +0 -0
  78. planar/files/storage/.local_directory.py.un~ +0 -0
  79. planar/files/storage/.test_local_directory.py.un~ +0 -0
  80. planar/files/storage/.test_s3.py.un~ +0 -0
  81. planar/files/storage/base.py +61 -0
  82. planar/files/storage/config.py +44 -0
  83. planar/files/storage/context.py +15 -0
  84. planar/files/storage/local_directory.py +188 -0
  85. planar/files/storage/s3.py +220 -0
  86. planar/files/storage/test_local_directory.py +162 -0
  87. planar/files/storage/test_s3.py +299 -0
  88. planar/files/test_files.py +283 -0
  89. planar/human/.human.py.un~ +0 -0
  90. planar/human/.test_human.py.un~ +0 -0
  91. planar/human/__init__.py +2 -0
  92. planar/human/human.py +458 -0
  93. planar/human/models.py +80 -0
  94. planar/human/test_human.py +385 -0
  95. planar/logging/.__init__.py.un~ +0 -0
  96. planar/logging/.attributes.py.un~ +0 -0
  97. planar/logging/.formatter.py.un~ +0 -0
  98. planar/logging/.logger.py.un~ +0 -0
  99. planar/logging/.otel.py.un~ +0 -0
  100. planar/logging/.tracer.py.un~ +0 -0
  101. planar/logging/__init__.py +10 -0
  102. planar/logging/attributes.py +54 -0
  103. planar/logging/context.py +14 -0
  104. planar/logging/formatter.py +113 -0
  105. planar/logging/logger.py +114 -0
  106. planar/logging/otel.py +51 -0
  107. planar/modeling/.mixin.py.un~ +0 -0
  108. planar/modeling/.storage.py.un~ +0 -0
  109. planar/modeling/__init__.py +0 -0
  110. planar/modeling/field_helpers.py +59 -0
  111. planar/modeling/json_schema_generator.py +94 -0
  112. planar/modeling/mixins/__init__.py +10 -0
  113. planar/modeling/mixins/auditable.py +52 -0
  114. planar/modeling/mixins/test_auditable.py +97 -0
  115. planar/modeling/mixins/test_timestamp.py +134 -0
  116. planar/modeling/mixins/test_uuid_primary_key.py +52 -0
  117. planar/modeling/mixins/timestamp.py +53 -0
  118. planar/modeling/mixins/uuid_primary_key.py +19 -0
  119. planar/modeling/orm/.planar_base_model.py.un~ +0 -0
  120. planar/modeling/orm/__init__.py +18 -0
  121. planar/modeling/orm/planar_base_entity.py +29 -0
  122. planar/modeling/orm/query_filter_builder.py +122 -0
  123. planar/modeling/orm/reexports.py +15 -0
  124. planar/object_config/.object_config.py.un~ +0 -0
  125. planar/object_config/__init__.py +11 -0
  126. planar/object_config/models.py +114 -0
  127. planar/object_config/object_config.py +378 -0
  128. planar/object_registry.py +100 -0
  129. planar/registry_items.py +65 -0
  130. planar/routers/.__init__.py.un~ +0 -0
  131. planar/routers/.agents_router.py.un~ +0 -0
  132. planar/routers/.crud.py.un~ +0 -0
  133. planar/routers/.decision.py.un~ +0 -0
  134. planar/routers/.event.py.un~ +0 -0
  135. planar/routers/.file_attachment.py.un~ +0 -0
  136. planar/routers/.files.py.un~ +0 -0
  137. planar/routers/.files_router.py.un~ +0 -0
  138. planar/routers/.human.py.un~ +0 -0
  139. planar/routers/.info.py.un~ +0 -0
  140. planar/routers/.models.py.un~ +0 -0
  141. planar/routers/.object_config_router.py.un~ +0 -0
  142. planar/routers/.rule.py.un~ +0 -0
  143. planar/routers/.test_object_config_router.py.un~ +0 -0
  144. planar/routers/.test_workflow_router.py.un~ +0 -0
  145. planar/routers/.workflow.py.un~ +0 -0
  146. planar/routers/__init__.py +13 -0
  147. planar/routers/agents_router.py +197 -0
  148. planar/routers/entity_router.py +143 -0
  149. planar/routers/event.py +91 -0
  150. planar/routers/files.py +142 -0
  151. planar/routers/human.py +151 -0
  152. planar/routers/info.py +131 -0
  153. planar/routers/models.py +170 -0
  154. planar/routers/object_config_router.py +133 -0
  155. planar/routers/rule.py +108 -0
  156. planar/routers/test_agents_router.py +174 -0
  157. planar/routers/test_object_config_router.py +367 -0
  158. planar/routers/test_routes_security.py +169 -0
  159. planar/routers/test_rule_router.py +470 -0
  160. planar/routers/test_workflow_router.py +274 -0
  161. planar/routers/workflow.py +468 -0
  162. planar/rules/.decorator.py.un~ +0 -0
  163. planar/rules/.runner.py.un~ +0 -0
  164. planar/rules/.test_rules.py.un~ +0 -0
  165. planar/rules/__init__.py +23 -0
  166. planar/rules/decorator.py +184 -0
  167. planar/rules/models.py +355 -0
  168. planar/rules/rule_configuration.py +191 -0
  169. planar/rules/runner.py +64 -0
  170. planar/rules/test_rules.py +750 -0
  171. planar/scaffold_templates/app/__init__.py.j2 +0 -0
  172. planar/scaffold_templates/app/db/entities.py.j2 +11 -0
  173. planar/scaffold_templates/app/flows/process_invoice.py.j2 +67 -0
  174. planar/scaffold_templates/main.py.j2 +13 -0
  175. planar/scaffold_templates/planar.dev.yaml.j2 +34 -0
  176. planar/scaffold_templates/planar.prod.yaml.j2 +28 -0
  177. planar/scaffold_templates/pyproject.toml.j2 +10 -0
  178. planar/security/.jwt_middleware.py.un~ +0 -0
  179. planar/security/auth_context.py +148 -0
  180. planar/security/authorization.py +388 -0
  181. planar/security/default_policies.cedar +77 -0
  182. planar/security/jwt_middleware.py +116 -0
  183. planar/security/security_context.py +18 -0
  184. planar/security/tests/test_authorization_context.py +78 -0
  185. planar/security/tests/test_cedar_basics.py +41 -0
  186. planar/security/tests/test_cedar_policies.py +158 -0
  187. planar/security/tests/test_jwt_principal_context.py +179 -0
  188. planar/session.py +40 -0
  189. planar/sse/.constants.py.un~ +0 -0
  190. planar/sse/.example.html.un~ +0 -0
  191. planar/sse/.hub.py.un~ +0 -0
  192. planar/sse/.model.py.un~ +0 -0
  193. planar/sse/.proxy.py.un~ +0 -0
  194. planar/sse/constants.py +1 -0
  195. planar/sse/example.html +126 -0
  196. planar/sse/hub.py +216 -0
  197. planar/sse/model.py +8 -0
  198. planar/sse/proxy.py +257 -0
  199. planar/task_local.py +37 -0
  200. planar/test_app.py +51 -0
  201. planar/test_cli.py +372 -0
  202. planar/test_config.py +512 -0
  203. planar/test_object_config.py +527 -0
  204. planar/test_object_registry.py +14 -0
  205. planar/test_sqlalchemy.py +158 -0
  206. planar/test_utils.py +105 -0
  207. planar/testing/.client.py.un~ +0 -0
  208. planar/testing/.memory_storage.py.un~ +0 -0
  209. planar/testing/.planar_test_client.py.un~ +0 -0
  210. planar/testing/.predictable_tracer.py.un~ +0 -0
  211. planar/testing/.synchronizable_tracer.py.un~ +0 -0
  212. planar/testing/.test_memory_storage.py.un~ +0 -0
  213. planar/testing/.workflow_observer.py.un~ +0 -0
  214. planar/testing/__init__.py +0 -0
  215. planar/testing/memory_storage.py +78 -0
  216. planar/testing/planar_test_client.py +54 -0
  217. planar/testing/synchronizable_tracer.py +153 -0
  218. planar/testing/test_memory_storage.py +143 -0
  219. planar/testing/workflow_observer.py +73 -0
  220. planar/utils.py +70 -0
  221. planar/workflows/.__init__.py.un~ +0 -0
  222. planar/workflows/.builtin_steps.py.un~ +0 -0
  223. planar/workflows/.concurrency_tracing.py.un~ +0 -0
  224. planar/workflows/.context.py.un~ +0 -0
  225. planar/workflows/.contrib.py.un~ +0 -0
  226. planar/workflows/.decorators.py.un~ +0 -0
  227. planar/workflows/.durable_test.py.un~ +0 -0
  228. planar/workflows/.errors.py.un~ +0 -0
  229. planar/workflows/.events.py.un~ +0 -0
  230. planar/workflows/.exceptions.py.un~ +0 -0
  231. planar/workflows/.execution.py.un~ +0 -0
  232. planar/workflows/.human.py.un~ +0 -0
  233. planar/workflows/.lock.py.un~ +0 -0
  234. planar/workflows/.misc.py.un~ +0 -0
  235. planar/workflows/.model.py.un~ +0 -0
  236. planar/workflows/.models.py.un~ +0 -0
  237. planar/workflows/.notifications.py.un~ +0 -0
  238. planar/workflows/.orchestrator.py.un~ +0 -0
  239. planar/workflows/.runtime.py.un~ +0 -0
  240. planar/workflows/.serialization.py.un~ +0 -0
  241. planar/workflows/.step.py.un~ +0 -0
  242. planar/workflows/.step_core.py.un~ +0 -0
  243. planar/workflows/.sub_workflow_runner.py.un~ +0 -0
  244. planar/workflows/.sub_workflow_scheduler.py.un~ +0 -0
  245. planar/workflows/.test_concurrency.py.un~ +0 -0
  246. planar/workflows/.test_concurrency_detection.py.un~ +0 -0
  247. planar/workflows/.test_human.py.un~ +0 -0
  248. planar/workflows/.test_lock_timeout.py.un~ +0 -0
  249. planar/workflows/.test_orchestrator.py.un~ +0 -0
  250. planar/workflows/.test_race_conditions.py.un~ +0 -0
  251. planar/workflows/.test_serialization.py.un~ +0 -0
  252. planar/workflows/.test_suspend_deserialization.py.un~ +0 -0
  253. planar/workflows/.test_workflow.py.un~ +0 -0
  254. planar/workflows/.tracing.py.un~ +0 -0
  255. planar/workflows/.types.py.un~ +0 -0
  256. planar/workflows/.util.py.un~ +0 -0
  257. planar/workflows/.utils.py.un~ +0 -0
  258. planar/workflows/.workflow.py.un~ +0 -0
  259. planar/workflows/.workflow_wrapper.py.un~ +0 -0
  260. planar/workflows/.wrappers.py.un~ +0 -0
  261. planar/workflows/__init__.py +42 -0
  262. planar/workflows/context.py +44 -0
  263. planar/workflows/contrib.py +190 -0
  264. planar/workflows/decorators.py +217 -0
  265. planar/workflows/events.py +185 -0
  266. planar/workflows/exceptions.py +34 -0
  267. planar/workflows/execution.py +198 -0
  268. planar/workflows/lock.py +229 -0
  269. planar/workflows/misc.py +5 -0
  270. planar/workflows/models.py +154 -0
  271. planar/workflows/notifications.py +96 -0
  272. planar/workflows/orchestrator.py +383 -0
  273. planar/workflows/query.py +256 -0
  274. planar/workflows/serialization.py +409 -0
  275. planar/workflows/step_core.py +373 -0
  276. planar/workflows/step_metadata.py +357 -0
  277. planar/workflows/step_testing_utils.py +86 -0
  278. planar/workflows/sub_workflow_runner.py +191 -0
  279. planar/workflows/test_concurrency_detection.py +120 -0
  280. planar/workflows/test_lock_timeout.py +140 -0
  281. planar/workflows/test_serialization.py +1195 -0
  282. planar/workflows/test_suspend_deserialization.py +231 -0
  283. planar/workflows/test_workflow.py +1967 -0
  284. planar/workflows/tracing.py +106 -0
  285. planar/workflows/wrappers.py +41 -0
  286. planar-0.5.0.dist-info/METADATA +285 -0
  287. planar-0.5.0.dist-info/RECORD +289 -0
  288. planar-0.5.0.dist-info/WHEEL +4 -0
  289. 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}
@@ -0,0 +1,10 @@
1
+ [project]
2
+ name = "{{ name }}"
3
+ version = "0.1.0"
4
+ requires-python = ">=3.12"
5
+ dependencies = [
6
+ "planar>=0.1.0",
7
+ ]
8
+
9
+ [[tool.uv.index]]
10
+ url = "https://coplane.github.io/planar/simple/"
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)