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
@@ -0,0 +1,378 @@
1
+ """
2
+ This module contains the ObjectConfiguration class, which is used to store and manage
3
+ the configuration of objects in the database.
4
+
5
+ "Object" is used to refer to agents, entities, rules, etc
6
+
7
+ See ConfigurableObjectType for the different types of objects that can be configured.
8
+
9
+ When a config is written for a particular object (uniquely identified by object_name and object_type),
10
+ this config will be used during workflow execution to drive the behaviour of that boject.
11
+
12
+ If no persisted config exists, then the workflow will fallback to the in-memory implementation
13
+ of that object as specified by the user in the planar sdk.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from datetime import datetime, timezone
19
+ from typing import Any, Callable, Generic, Sequence, Type, TypeVar, cast
20
+ from uuid import UUID
21
+
22
+ from pydantic import BaseModel
23
+ from sqlmodel import col, select, update
24
+
25
+ from planar.logging import get_logger
26
+ from planar.object_config.models import (
27
+ ConfigDiagnostics,
28
+ ConfigurableObjectType,
29
+ ObjectConfiguration,
30
+ ObjectConfigurationBase,
31
+ )
32
+ from planar.session import get_session
33
+
34
+ # Special case: UUID with all zeros means revert to in-memory default (set all configs inactive)
35
+ DEFAULT_UUID = UUID("00000000-0000-0000-0000-000000000000")
36
+
37
+ logger = get_logger(__name__)
38
+
39
+
40
+ T = TypeVar("T", bound="BaseModel")
41
+ V = TypeVar("V")
42
+
43
+
44
+ def default_validate_config(name: str, config: BaseModel) -> ConfigDiagnostics:
45
+ return ConfigDiagnostics(is_valid=True, issues=[])
46
+
47
+
48
+ class ObjectConfigurationIO(Generic[T, V]):
49
+ """Abstract base class for reading and writing different object configurations.
50
+
51
+ This class provides a framework for creating concrete implementations
52
+ that know how to deserialize configuration data using specific Pydantic models.
53
+ """
54
+
55
+ def __init__(
56
+ self,
57
+ model_class: Type[T],
58
+ object_type: ConfigurableObjectType,
59
+ validate_config: Callable[
60
+ [str, T], ConfigDiagnostics
61
+ ] = default_validate_config,
62
+ ):
63
+ """Initialize the reader with a Pydantic model class.
64
+
65
+ Args:
66
+ model_class: The Pydantic model class used for deserialization
67
+ object_type: The type of object this configuration is for
68
+ """
69
+ self.model_class = model_class
70
+ self.object_type = object_type
71
+ self.validate_config = validate_config
72
+
73
+ async def _read_configs(
74
+ self, object_name: str, limit: int | None = 20
75
+ ) -> Sequence[ObjectConfiguration[T]]:
76
+ """Read all configurations from the database for a object with schema validation warnings.
77
+
78
+ Intended for internal use only since the public API requires a default (in-memory) configuration
79
+
80
+ Args:
81
+ object_name: The name of the object to read configurations for
82
+ limit: The maximum number of configurations to read
83
+
84
+ Returns:
85
+ A ConfigurationResult containing all configurations ordered by version (descending) and any schema warnings
86
+ """
87
+ session = get_session()
88
+
89
+ async with session.begin_read():
90
+ # Query for all versions of the object configuration, ordered by version descending
91
+ statement = (
92
+ select(ObjectConfiguration)
93
+ .where(ObjectConfiguration.object_name == object_name)
94
+ .where(ObjectConfiguration.object_type == self.object_type)
95
+ .order_by(col(ObjectConfiguration.version).desc())
96
+ .limit(limit)
97
+ )
98
+
99
+ cfgs = (await session.exec(statement)).all()
100
+
101
+ for cfg in cfgs:
102
+ cfg.data = self.model_class.model_validate(cfg.data)
103
+
104
+ logger.debug(
105
+ f"Read {len(cfgs)} configurations for object '{object_name}' of type '{self.object_type}'."
106
+ )
107
+
108
+ return cfgs
109
+
110
+ async def read_configs_with_default(
111
+ self, object_name: str, default_value: T
112
+ ) -> list[ObjectConfigurationBase[T]]:
113
+ """Read all configurations for a object with a default configuration.
114
+
115
+ Args:
116
+ object_name: The name of the object for which to read configurations
117
+ default_value: The default configuration to use if no configurations are found
118
+
119
+ Returns:
120
+ A ConfigurationResult containing all configurations ordered by version (descending)
121
+ """
122
+ logger.debug(
123
+ "reading configs with default for object",
124
+ object_name=object_name,
125
+ object_type=self.object_type,
126
+ )
127
+
128
+ config_list = await self._read_configs(object_name)
129
+
130
+ default_config_active = all(not config.active for config in config_list)
131
+ logger.debug(
132
+ "default config active status",
133
+ object_name=object_name,
134
+ is_active=default_config_active,
135
+ )
136
+
137
+ default_config = ObjectConfigurationBase[T].model_validate(
138
+ {
139
+ "id": DEFAULT_UUID,
140
+ "object_type": self.object_type,
141
+ "object_name": object_name,
142
+ "version": 0,
143
+ "active": default_config_active,
144
+ "created_at": datetime.now(timezone.utc),
145
+ "data": default_value,
146
+ }
147
+ )
148
+
149
+ validated_configs: list[ObjectConfigurationBase[T]] = []
150
+
151
+ for config in config_list:
152
+ validated_configs.append(
153
+ ObjectConfigurationBase[T].model_validate(
154
+ {
155
+ "id": config.id,
156
+ "object_name": config.object_name,
157
+ "object_type": config.object_type,
158
+ "created_at": config.created_at,
159
+ "version": config.version,
160
+ "data": self.model_class.model_validate(config.data),
161
+ "active": config.active,
162
+ }
163
+ )
164
+ )
165
+
166
+ validated_configs.append(default_config)
167
+
168
+ return validated_configs
169
+
170
+ async def write_config(self, object_name: str, config: T) -> ObjectConfiguration:
171
+ """Write the configuration to a ObjectConfiguration.
172
+
173
+ Args:
174
+ object_name: The name of the object to write configuration for
175
+ config: The Pydantic model instance to write
176
+
177
+ Returns:
178
+ A ConfigurationResult containing the written configuration
179
+ """
180
+ logger.debug(
181
+ "writing config for object",
182
+ object_name=object_name,
183
+ object_type=self.object_type,
184
+ )
185
+ session = get_session()
186
+
187
+ async with session.begin():
188
+ existing_configs = await self._read_configs(object_name, limit=1)
189
+
190
+ if not existing_configs:
191
+ version = 1
192
+ else:
193
+ # Get the highest version number and increment it
194
+ version = existing_configs[0].version + 1
195
+
196
+ logger.debug(
197
+ "new configuration version",
198
+ object_name=object_name,
199
+ version=version,
200
+ )
201
+
202
+ result = self.validate_config(object_name, config)
203
+
204
+ if not result.is_valid:
205
+ raise ConfigValidationError(object_name, self.object_type, result)
206
+
207
+ # The JSON codec will handle converting the BaseModel to JSON string
208
+ object_config = ObjectConfiguration(
209
+ object_name=object_name,
210
+ object_type=self.object_type,
211
+ data=config, # type: ignore[arg-type]
212
+ version=version,
213
+ )
214
+
215
+ session.add(object_config)
216
+ logger.info(
217
+ "configuration written to database",
218
+ version=version,
219
+ object_name=object_name,
220
+ config_id=object_config.id,
221
+ )
222
+ return object_config
223
+
224
+ async def promote_config(
225
+ self, config_id: UUID, object_name: str | None = None
226
+ ) -> None:
227
+ """Promote a specific configuration to be the active one.
228
+
229
+ Args:
230
+ config_id: The UUID of the configuration to promote.
231
+ Use UUID('00000000-0000-0000-0000-000000000000') to revert to default implementation.
232
+ object_name: Required when using the default UUID to specify which object to revert
233
+
234
+ Returns:
235
+ A ConfigurationResult containing all configurations for the object
236
+
237
+ Raises:
238
+ ConfigNotFoundError: If the configuration is not found
239
+ """
240
+ logger.debug(
241
+ "promoting config",
242
+ config_id=config_id,
243
+ object_name=object_name,
244
+ object_type=self.object_type,
245
+ )
246
+ session = get_session()
247
+ async with session.begin():
248
+ # Edge case: revert to default in-memory configuration by setting all configs to inactive
249
+ if config_id == DEFAULT_UUID:
250
+ if not object_name:
251
+ raise ValueError(
252
+ "object_name is required when reverting to default configuration"
253
+ )
254
+ logger.info(
255
+ "reverting object to default configuration",
256
+ object_name=object_name,
257
+ object_type=self.object_type,
258
+ )
259
+ update_query = (
260
+ update(ObjectConfiguration)
261
+ .where(col(ObjectConfiguration.object_name) == object_name)
262
+ .where(col(ObjectConfiguration.object_type) == self.object_type)
263
+ .values(active=False)
264
+ )
265
+
266
+ await session.exec(cast(Any, update_query))
267
+ return
268
+
269
+ # First, find the configuration to promote
270
+ target_config = (
271
+ await session.exec(
272
+ select(ObjectConfiguration)
273
+ .where(ObjectConfiguration.id == config_id)
274
+ .where(ObjectConfiguration.object_type == self.object_type)
275
+ )
276
+ ).first()
277
+
278
+ if not target_config:
279
+ logger.warning(
280
+ "config id not found during promotion",
281
+ config_id=config_id,
282
+ object_type=self.object_type,
283
+ )
284
+ raise ConfigNotFoundError(config_id, self.object_type)
285
+
286
+ logger.info(
287
+ "found target config to promote",
288
+ version=target_config.version,
289
+ object_name=target_config.object_name,
290
+ )
291
+
292
+ config_data = self.model_class.model_validate(target_config.data)
293
+ result = self.validate_config(target_config.object_name, config_data)
294
+
295
+ if not result.is_valid:
296
+ raise ConfigValidationError(
297
+ target_config.object_name, self.object_type, result
298
+ )
299
+
300
+ # Set all configurations for this object to inactive
301
+ all_configs = (
302
+ await session.exec(
303
+ select(ObjectConfiguration)
304
+ .where(ObjectConfiguration.object_name == target_config.object_name)
305
+ .where(ObjectConfiguration.object_type == self.object_type)
306
+ )
307
+ ).all()
308
+
309
+ for config_item in all_configs:
310
+ config_item.active = False
311
+
312
+ # Set the target configuration to active
313
+ target_config.active = True
314
+ logger.info(
315
+ "config is now active",
316
+ version=target_config.version,
317
+ object_name=target_config.object_name,
318
+ )
319
+
320
+ # Add all modified configs to the session
321
+ for config_item in (
322
+ all_configs
323
+ ): # Ensure target_config is also added if it was part of all_configs
324
+ session.add(config_item)
325
+
326
+ # Explicitly add target_config if it wasn't part of all_configs (should not happen with current logic)
327
+ # or if it was modified and needs to be re-added.
328
+ if (
329
+ target_config not in all_configs
330
+ ): # Should not be true if logic is correct
331
+ session.add(target_config)
332
+
333
+ return
334
+
335
+
336
+ class ConfigValidationErrorResponse(BaseModel):
337
+ """Response model for configuration validation errors."""
338
+
339
+ error: str
340
+ object_name: str
341
+ object_type: str
342
+ diagnostics: ConfigDiagnostics
343
+
344
+
345
+ class ConfigValidationError(Exception):
346
+ """Raised when object configuration validation fails."""
347
+
348
+ def __init__(
349
+ self,
350
+ object_name: str,
351
+ object_type: ConfigurableObjectType,
352
+ diagnostics: ConfigDiagnostics,
353
+ ):
354
+ self.object_name = object_name
355
+ self.object_type = object_type
356
+ self.diagnostics = diagnostics
357
+
358
+ super().__init__(f"Validation failed for {object_type} '{object_name}'")
359
+
360
+ def to_api_response(self) -> ConfigValidationErrorResponse:
361
+ """Convert ValidationError to a JSON-serializable dictionary for API responses."""
362
+ return ConfigValidationErrorResponse(
363
+ error="ValidationError",
364
+ object_name=self.object_name,
365
+ object_type=self.object_type.value,
366
+ diagnostics=self.diagnostics,
367
+ )
368
+
369
+
370
+ class ConfigNotFoundError(Exception):
371
+ """Raised when a configuration with the specified ID is not found."""
372
+
373
+ def __init__(self, invalid_id, object_type):
374
+ self.invalid_id = invalid_id
375
+ self.object_type = object_type
376
+ super().__init__(
377
+ f"Configuration with ID {invalid_id} and object_type {object_type} not found"
378
+ )
@@ -0,0 +1,100 @@
1
+ """
2
+ Used to track what objects have been registered with a PlanarAppinstance
3
+
4
+ Note that in planar/workflows/execution.py, we also have a registry of workflows
5
+ called _WORKFLOW_FUNCTION_REGISTRY. However, that registry is internal to the implementation
6
+ of workflows. Do not use that registry.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Type
12
+
13
+ from planar.ai.agent import Agent
14
+ from planar.modeling.orm.planar_base_entity import PlanarBaseEntity
15
+ from planar.registry_items import RegisteredWorkflow
16
+ from planar.rules.models import Rule
17
+ from planar.workflows.decorators import WorkflowWrapper
18
+
19
+
20
+ # singleton
21
+ class ObjectRegistry:
22
+ _instance: ObjectRegistry | None = None
23
+
24
+ _rules: dict[str, Rule]
25
+ _workflows: dict[str, RegisteredWorkflow]
26
+ _entities: dict[str, Type[PlanarBaseEntity]]
27
+ _agents: dict[str, Agent]
28
+
29
+ def __new__(cls) -> ObjectRegistry:
30
+ if cls._instance is None:
31
+ cls._instance = super(ObjectRegistry, cls).__new__(cls)
32
+
33
+ cls._instance._rules = {}
34
+ cls._instance._workflows = {}
35
+ cls._instance._entities = {}
36
+ cls._instance._agents = {}
37
+
38
+ return cls._instance
39
+
40
+ @staticmethod
41
+ def get_instance() -> ObjectRegistry:
42
+ if ObjectRegistry._instance is None:
43
+ return ObjectRegistry()
44
+
45
+ return ObjectRegistry._instance
46
+
47
+ def register(
48
+ self, obj: Type[PlanarBaseEntity] | "WorkflowWrapper" | "Agent" | "Rule"
49
+ ) -> None:
50
+ """
51
+ Register a PlanarBaseEntity or WorkflowWrapper object.
52
+ Adding the same object more than once is a no-op.
53
+
54
+ Note that when registering a WorkflowWrapper, its rules are also registered as there is no
55
+ explicit registration of rules API. It's implicit the moment a rule function (@rule) is invoked
56
+ from within a workflow.
57
+ """
58
+
59
+ if isinstance(obj, type) and issubclass(obj, PlanarBaseEntity):
60
+ self._entities[obj.__name__] = obj
61
+ elif isinstance(obj, WorkflowWrapper):
62
+ self._workflows[obj.function_name] = RegisteredWorkflow.from_workflow(obj)
63
+ elif isinstance(obj, Agent):
64
+ self._agents[obj.name] = obj
65
+ elif isinstance(obj, Rule):
66
+ self._rules[obj.name] = obj
67
+
68
+ def get_entities(self) -> list[Type[PlanarBaseEntity]]:
69
+ """
70
+ Get all registered PlanarBaseEntity objects.
71
+ """
72
+ return list(self._entities.values())
73
+
74
+ def get_workflows(self) -> list[RegisteredWorkflow]:
75
+ """
76
+ Get all registered WorkflowWrapper objects.
77
+ """
78
+ return list(self._workflows.values())
79
+
80
+ def get_rules(self) -> list[Rule]:
81
+ """
82
+ Get all registered rule objects.
83
+ """
84
+ return list(self._rules.values())
85
+
86
+ def get_agents(self) -> list[Agent]:
87
+ """
88
+ Get all registered Agent objects.
89
+ """
90
+ return list(self._agents.values())
91
+
92
+ def reset(self) -> None:
93
+ """
94
+ Reset the registry by clearing all registered objects.
95
+ This is useful for testing to ensure clean state between tests.
96
+ """
97
+ self._rules = {}
98
+ self._workflows = {}
99
+ self._entities = {}
100
+ self._agents = {}
@@ -0,0 +1,65 @@
1
+ """
2
+ Registry item classes for tracking registered objects.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import inspect
8
+ from dataclasses import dataclass
9
+ from typing import Any, Type, cast
10
+
11
+ from pydantic import BaseModel, create_model
12
+
13
+ from planar.modeling.json_schema_generator import (
14
+ generate_json_schema_for_input_parameters,
15
+ generate_json_schema_for_output_parameters,
16
+ )
17
+ from planar.utils import snake_case_to_camel_case
18
+ from planar.workflows.decorators import WorkflowWrapper
19
+
20
+
21
+ def create_pydantic_model_for_workflow(workflow: WorkflowWrapper) -> Type[BaseModel]:
22
+ start_params = inspect.signature(workflow.original_fn).parameters
23
+ start_fields = cast(
24
+ Any,
25
+ {
26
+ name: (
27
+ param.annotation,
28
+ ... if param.default == param.empty else param.default,
29
+ )
30
+ for name, param in start_params.items()
31
+ },
32
+ )
33
+
34
+ simple_name = workflow.function_name.split(".")[-1]
35
+ start_model_name = f"{snake_case_to_camel_case(simple_name)}StartRequest"
36
+
37
+ return create_model(start_model_name, **start_fields)
38
+
39
+
40
+ @dataclass(eq=False)
41
+ class RegisteredWorkflow:
42
+ """Lightweight record of a registered workflow."""
43
+
44
+ obj: "WorkflowWrapper"
45
+ name: str
46
+ description: str
47
+ input_schema: dict[str, Any]
48
+ output_schema: dict[str, Any]
49
+ pydantic_model: Type[BaseModel]
50
+
51
+ @staticmethod
52
+ def from_workflow(workflow: "WorkflowWrapper") -> "RegisteredWorkflow":
53
+ """Create a RegisteredWorkflow from a WorkflowWrapper."""
54
+ return RegisteredWorkflow(
55
+ obj=workflow,
56
+ name=workflow.function_name,
57
+ description=workflow.__doc__ or "No docstring provided for this function.",
58
+ input_schema=generate_json_schema_for_input_parameters(
59
+ workflow.original_fn
60
+ ),
61
+ output_schema=generate_json_schema_for_output_parameters(
62
+ workflow.original_fn
63
+ ),
64
+ pydantic_model=create_pydantic_model_for_workflow(workflow),
65
+ )
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,13 @@
1
+ from .event import create_workflow_event_routes
2
+ from .files import create_files_router
3
+ from .human import create_human_task_routes
4
+ from .info import create_info_router
5
+ from .workflow import create_workflow_router
6
+
7
+ __all__ = [
8
+ "create_workflow_event_routes",
9
+ "create_human_task_routes",
10
+ "create_workflow_router",
11
+ "create_info_router",
12
+ "create_files_router",
13
+ ]