cortex-agent-sdk 0.0.2__tar.gz → 0.0.3__tar.gz

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 (45) hide show
  1. {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.3}/PKG-INFO +30 -13
  2. {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.3}/README.md +29 -12
  3. {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.3}/pyproject.toml +1 -1
  4. {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.3}/src/cortex_agent_sdk/agent.py +5 -2
  5. {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.3}/src/cortex_agent_sdk/engine.py +19 -15
  6. cortex_agent_sdk-0.0.3/src/cortex_agent_sdk/google/__init__.py +1 -0
  7. {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.3}/src/cortex_agent_sdk/history/models.py +46 -37
  8. {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.3}/src/cortex_agent_sdk/history/transform.py +2 -3
  9. cortex_agent_sdk-0.0.3/src/cortex_agent_sdk/immutable.py +75 -0
  10. {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.3}/src/cortex_agent_sdk/openai/__init__.py +1 -0
  11. {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.3}/src/cortex_agent_sdk/openai/engine.py +25 -17
  12. cortex_agent_sdk-0.0.3/src/cortex_agent_sdk/openai/models.py +57 -0
  13. {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.3}/src/cortex_agent_sdk/redis/store.py +32 -17
  14. {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.3}/src/cortex_agent_sdk/tools/contracts.py +3 -4
  15. {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.3}/src/cortex_agent_sdk/tools/execution.py +2 -6
  16. {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.3}/src/cortex_agent_sdk/tools/models.py +12 -12
  17. cortex_agent_sdk-0.0.2/src/cortex_agent_sdk/google/__init__.py +0 -1
  18. cortex_agent_sdk-0.0.2/src/cortex_agent_sdk/immutable.py +0 -54
  19. {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.3}/.gitignore +0 -0
  20. {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.3}/LICENSE +0 -0
  21. {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.3}/src/cortex_agent_sdk/__init__.py +0 -0
  22. {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.3}/src/cortex_agent_sdk/errores/__init__.py +0 -0
  23. {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.3}/src/cortex_agent_sdk/errores/catalogo.py +0 -0
  24. {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.3}/src/cortex_agent_sdk/errores/excepcion.py +0 -0
  25. {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.3}/src/cortex_agent_sdk/gateway.py +0 -0
  26. {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.3}/src/cortex_agent_sdk/history/__init__.py +0 -0
  27. {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.3}/src/cortex_agent_sdk/history/pipeline.py +0 -0
  28. {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.3}/src/cortex_agent_sdk/hooks.py +0 -0
  29. {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.3}/src/cortex_agent_sdk/lifecycle.py +0 -0
  30. {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.3}/src/cortex_agent_sdk/openai/options.py +0 -0
  31. {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.3}/src/cortex_agent_sdk/postgres/__init__.py +0 -0
  32. {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.3}/src/cortex_agent_sdk/postgres/store.py +0 -0
  33. {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.3}/src/cortex_agent_sdk/py.typed +0 -0
  34. {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.3}/src/cortex_agent_sdk/redis/__init__.py +0 -0
  35. {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.3}/src/cortex_agent_sdk/redis/scripts.py +0 -0
  36. {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.3}/src/cortex_agent_sdk/results.py +0 -0
  37. {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.3}/src/cortex_agent_sdk/runtime.py +0 -0
  38. {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.3}/src/cortex_agent_sdk/sessions/__init__.py +0 -0
  39. {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.3}/src/cortex_agent_sdk/sessions/codec.py +0 -0
  40. {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.3}/src/cortex_agent_sdk/sessions/lease.py +0 -0
  41. {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.3}/src/cortex_agent_sdk/sessions/memory.py +0 -0
  42. {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.3}/src/cortex_agent_sdk/sessions/models.py +0 -0
  43. {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.3}/src/cortex_agent_sdk/sessions/store.py +0 -0
  44. {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.3}/src/cortex_agent_sdk/tools/__init__.py +0 -0
  45. {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.3}/src/cortex_agent_sdk/tools/decorators.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cortex-agent-sdk
3
- Version: 0.0.2
3
+ Version: 0.0.3
4
4
  Summary: SDK async y multiproveedor para construir agentes con control explícito
5
5
  Project-URL: Repository, https://github.com/epok200/cortex_agent_sdk
6
6
  Project-URL: Issues, https://github.com/epok200/cortex_agent_sdk/issues
@@ -159,6 +159,7 @@ renovable, fencing token y compare-and-swap.
159
159
  Redis no requiere inicialización de schema:
160
160
 
161
161
  ```python
162
+ import asyncio
162
163
  import os
163
164
 
164
165
  from cortex_agent_sdk import Agent
@@ -166,26 +167,38 @@ from cortex_agent_sdk.openai import OpenAIEngine
166
167
  from cortex_agent_sdk.redis import RedisSessionStore
167
168
 
168
169
 
169
- store = RedisSessionStore(os.environ["REDIS_URL"])
170
- agent = Agent(
171
- OpenAIEngine("gpt-5.6-luna"),
172
- session_store=store,
173
- own_session_store=True,
174
- )
175
- result = await agent.run("Hola", session_id="producto:tenant:usuario")
176
- await agent.aclose()
170
+ async def main() -> None:
171
+ store = RedisSessionStore(os.environ["REDIS_URL"])
172
+ async with Agent(
173
+ OpenAIEngine("gpt-5.6-luna"),
174
+ session_store=store,
175
+ own_session_store=True,
176
+ ) as agent:
177
+ result = await agent.run("Hola", session_id="producto:tenant:usuario")
178
+ print(result.text)
179
+
180
+
181
+ asyncio.run(main())
177
182
  ```
178
183
 
179
184
  PostgreSQL exige crear su tabla de forma explícita una vez:
180
185
 
181
186
  ```python
187
+ import asyncio
182
188
  import os
183
189
 
184
190
  from cortex_agent_sdk.postgres import PostgresSessionStore
185
191
 
186
192
 
187
- store = PostgresSessionStore(os.environ["POSTGRES_URL"])
188
- await store.setup()
193
+ async def main() -> None:
194
+ store = PostgresSessionStore(os.environ["POSTGRES_URL"])
195
+ try:
196
+ await store.setup()
197
+ finally:
198
+ await store.aclose()
199
+
200
+
201
+ asyncio.run(main())
189
202
  ```
190
203
 
191
204
  Una tarea periódica puede ejecutar `await store.cleanup_expired()` para vaciar historiales vencidos
@@ -193,8 +206,12 @@ que nunca volvieron a solicitarse. El row mínimo permanece para conservar el fe
193
206
 
194
207
  `Agent.aclose()` hace un cierre ordenado: deja de aceptar turnos nuevos, espera los turnos activos y
195
208
  después cierra los recursos que posee. El límite se configura con
196
- `AgentOptions.shutdown_timeout_seconds`. Si vence, el SDK no cancela el turno ni cierra conexiones;
197
- regresa `RUNTIME_CIERRE_TIMEOUT` para que la aplicación pueda reintentar el cierre.
209
+ `AgentOptions.shutdown_timeout_seconds` y cubre tanto el drenado como el cierre físico. Si vence
210
+ mientras hay un turno activo, el SDK no lo cancela ni empieza a cerrar recursos. Si vence durante el
211
+ cierre físico, algunos recursos podrían haberse cerrado ya. El timeout es un presupuesto de cierre,
212
+ no una garantía estricta de tiempo de pared: un finalizador que resista la cancelación puede retrasar
213
+ el retorno para no abandonar recursos a medias. Al excederlo regresa `RUNTIME_CIERRE_TIMEOUT` y una
214
+ segunda llamada a `aclose()` reintenta lo pendiente.
198
215
 
199
216
  Una sesión dañada o deliberadamente descartada se elimina mediante
200
217
  `await agent.reset_session(session_id)`.
@@ -125,6 +125,7 @@ renovable, fencing token y compare-and-swap.
125
125
  Redis no requiere inicialización de schema:
126
126
 
127
127
  ```python
128
+ import asyncio
128
129
  import os
129
130
 
130
131
  from cortex_agent_sdk import Agent
@@ -132,26 +133,38 @@ from cortex_agent_sdk.openai import OpenAIEngine
132
133
  from cortex_agent_sdk.redis import RedisSessionStore
133
134
 
134
135
 
135
- store = RedisSessionStore(os.environ["REDIS_URL"])
136
- agent = Agent(
137
- OpenAIEngine("gpt-5.6-luna"),
138
- session_store=store,
139
- own_session_store=True,
140
- )
141
- result = await agent.run("Hola", session_id="producto:tenant:usuario")
142
- await agent.aclose()
136
+ async def main() -> None:
137
+ store = RedisSessionStore(os.environ["REDIS_URL"])
138
+ async with Agent(
139
+ OpenAIEngine("gpt-5.6-luna"),
140
+ session_store=store,
141
+ own_session_store=True,
142
+ ) as agent:
143
+ result = await agent.run("Hola", session_id="producto:tenant:usuario")
144
+ print(result.text)
145
+
146
+
147
+ asyncio.run(main())
143
148
  ```
144
149
 
145
150
  PostgreSQL exige crear su tabla de forma explícita una vez:
146
151
 
147
152
  ```python
153
+ import asyncio
148
154
  import os
149
155
 
150
156
  from cortex_agent_sdk.postgres import PostgresSessionStore
151
157
 
152
158
 
153
- store = PostgresSessionStore(os.environ["POSTGRES_URL"])
154
- await store.setup()
159
+ async def main() -> None:
160
+ store = PostgresSessionStore(os.environ["POSTGRES_URL"])
161
+ try:
162
+ await store.setup()
163
+ finally:
164
+ await store.aclose()
165
+
166
+
167
+ asyncio.run(main())
155
168
  ```
156
169
 
157
170
  Una tarea periódica puede ejecutar `await store.cleanup_expired()` para vaciar historiales vencidos
@@ -159,8 +172,12 @@ que nunca volvieron a solicitarse. El row mínimo permanece para conservar el fe
159
172
 
160
173
  `Agent.aclose()` hace un cierre ordenado: deja de aceptar turnos nuevos, espera los turnos activos y
161
174
  después cierra los recursos que posee. El límite se configura con
162
- `AgentOptions.shutdown_timeout_seconds`. Si vence, el SDK no cancela el turno ni cierra conexiones;
163
- regresa `RUNTIME_CIERRE_TIMEOUT` para que la aplicación pueda reintentar el cierre.
175
+ `AgentOptions.shutdown_timeout_seconds` y cubre tanto el drenado como el cierre físico. Si vence
176
+ mientras hay un turno activo, el SDK no lo cancela ni empieza a cerrar recursos. Si vence durante el
177
+ cierre físico, algunos recursos podrían haberse cerrado ya. El timeout es un presupuesto de cierre,
178
+ no una garantía estricta de tiempo de pared: un finalizador que resista la cancelación puede retrasar
179
+ el retorno para no abandonar recursos a medias. Al excederlo regresa `RUNTIME_CIERRE_TIMEOUT` y una
180
+ segunda llamada a `aclose()` reintenta lo pendiente.
164
181
 
165
182
  Una sesión dañada o deliberadamente descartada se elimina mediante
166
183
  `await agent.reset_session(session_id)`.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cortex-agent-sdk"
3
- version = "0.0.2"
3
+ version = "0.0.3"
4
4
  description = "SDK async y multiproveedor para construir agentes con control explícito"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -43,7 +43,10 @@ class Agent:
43
43
  self._options = options or AgentOptions()
44
44
  self._history = HistoryPipeline(history_transform, self._options.max_history_turns)
45
45
  self._hooks = HookChain(hooks or AgentHooks(), self._options.hook_timeout_seconds)
46
- self._session_store = session_store or MemorySessionStore()
46
+ if session_store is None:
47
+ self._session_store = MemorySessionStore()
48
+ else:
49
+ self._session_store = session_store
47
50
  self._own_engine = own_engine
48
51
  self._own_session_store = session_store is None or own_session_store
49
52
  self._lifecycle = AgentLifecycle()
@@ -65,7 +68,7 @@ class Agent:
65
68
  instructions: str | None = None,
66
69
  ) -> AgentResult:
67
70
  async with self._lifecycle.run():
68
- if not text:
71
+ if not text.strip():
69
72
  raise AppError(CodigoError.CONFIG_INVALIDA, "text no puede estar vacío")
70
73
 
71
74
  tool_set = ToolSet.build((*self._static_tools, *tuple(tools)))
@@ -1,12 +1,12 @@
1
1
  from collections.abc import Mapping
2
2
  from dataclasses import dataclass
3
3
  from types import MappingProxyType
4
- from typing import Protocol, cast, runtime_checkable
4
+ from typing import Protocol, runtime_checkable
5
5
 
6
6
  from pydantic import BaseModel, ConfigDict, JsonValue, field_serializer, field_validator
7
7
 
8
8
  from cortex_agent_sdk.history.models import ToolCallPart, Turn
9
- from cortex_agent_sdk.immutable import FrozenJsonValue, freeze_json, thaw_json
9
+ from cortex_agent_sdk.immutable import JsonObject, freeze_json_object, thaw_json_object
10
10
  from cortex_agent_sdk.tools.models import ToolSpec
11
11
 
12
12
 
@@ -15,25 +15,22 @@ class ToolCall(BaseModel):
15
15
 
16
16
  call_id: str
17
17
  name: str
18
- arguments: Mapping[str, JsonValue]
18
+ arguments: JsonObject
19
19
 
20
20
  @field_validator("arguments", mode="after")
21
21
  @classmethod
22
22
  def freeze_arguments(
23
23
  cls,
24
- value: Mapping[str, JsonValue],
25
- ) -> Mapping[str, JsonValue]:
26
- frozen = freeze_json(value)
27
- if not isinstance(frozen, Mapping):
28
- raise TypeError("arguments debe ser un objeto")
29
- return cast(Mapping[str, JsonValue], frozen)
24
+ value: JsonObject,
25
+ ) -> JsonObject:
26
+ return freeze_json_object(value)
30
27
 
31
28
  @field_serializer("arguments", when_used="json")
32
29
  def serialize_arguments(
33
30
  self,
34
- value: Mapping[str, JsonValue],
35
- ) -> dict[str, object]:
36
- return {key: thaw_json(cast(FrozenJsonValue, item)) for key, item in value.items()}
31
+ value: JsonObject,
32
+ ) -> dict[str, JsonValue]:
33
+ return thaw_json_object(value)
37
34
 
38
35
 
39
36
  @dataclass(frozen=True, slots=True)
@@ -117,7 +114,7 @@ class EngineResult:
117
114
  isinstance(call, ToolCall) for call in self.tool_calls
118
115
  )
119
116
  valid_structured = self.structured is None or isinstance(self.structured, BaseModel)
120
- if not isinstance(self.turn, Turn) or not valid_calls:
117
+ if not isinstance(self.turn, Turn) or self.turn.role != "assistant" or not valid_calls:
121
118
  raise TypeError("EngineResult inválido")
122
119
  turn_calls = tuple(part for part in self.turn.parts if isinstance(part, ToolCallPart))
123
120
  aligned_calls = len(turn_calls) == len(self.tool_calls) and all(
@@ -128,7 +125,15 @@ class EngineResult:
128
125
  )
129
126
  if not aligned_calls:
130
127
  raise TypeError("tool_calls no coincide con el turno")
131
- if not isinstance(self.usage, Usage) or not self.provider or not self.model:
128
+ valid_identity = (
129
+ isinstance(self.provider, str)
130
+ and bool(self.provider)
131
+ and isinstance(self.model, str)
132
+ and bool(self.model)
133
+ )
134
+ provider_state = self.turn.provider_state
135
+ state_matches = provider_state is None or provider_state.provider == self.provider
136
+ if not isinstance(self.usage, Usage) or not valid_identity or not state_matches:
132
137
  raise TypeError("EngineResult inválido")
133
138
  if self.stop_reason is not None and not isinstance(self.stop_reason, str):
134
139
  raise TypeError("stop_reason inválido")
@@ -147,4 +152,3 @@ class ModelEngine(Protocol):
147
152
  async def generate(self, request: EngineRequest) -> EngineResult: ...
148
153
 
149
154
  async def aclose(self) -> None: ...
150
-
@@ -0,0 +1 @@
1
+ """Espacio reservado para el futuro engine de Google."""
@@ -1,21 +1,28 @@
1
1
  import base64
2
2
  from collections.abc import Mapping
3
- from typing import Annotated, Literal, cast
4
-
5
- from pydantic import BaseModel, ConfigDict, Field, JsonValue, field_serializer, field_validator
3
+ from typing import Annotated, Literal, Self
4
+
5
+ from pydantic import (
6
+ BaseModel,
7
+ ConfigDict,
8
+ Field,
9
+ JsonValue,
10
+ field_serializer,
11
+ field_validator,
12
+ model_validator,
13
+ )
6
14
 
7
15
  from cortex_agent_sdk.immutable import (
8
- FrozenJsonValue,
9
- FrozenProviderValue,
10
- freeze_json,
11
- freeze_provider,
12
- thaw_json,
16
+ JsonObject,
17
+ ProviderItem,
18
+ ProviderValue,
19
+ freeze_json_object,
20
+ freeze_provider_item,
21
+ thaw_json_object,
13
22
  )
14
23
 
15
24
  _BYTES_TAG = "$cortex.bytes"
16
25
 
17
- ProviderValue = FrozenProviderValue
18
-
19
26
 
20
27
  class ProviderState(BaseModel):
21
28
  """Items nativos necesarios para continuar sin pérdida con un provider."""
@@ -23,7 +30,7 @@ class ProviderState(BaseModel):
23
30
  model_config = ConfigDict(extra="forbid", frozen=True)
24
31
 
25
32
  provider: str
26
- items: tuple[Mapping[str, ProviderValue], ...]
33
+ items: tuple[ProviderItem, ...]
27
34
 
28
35
  @field_validator("items", mode="before")
29
36
  @classmethod
@@ -34,14 +41,14 @@ class ProviderState(BaseModel):
34
41
  @classmethod
35
42
  def freeze_items(
36
43
  cls,
37
- value: tuple[Mapping[str, ProviderValue], ...],
38
- ) -> tuple[Mapping[str, ProviderValue], ...]:
39
- return tuple(_freeze_provider_item(item) for item in value)
44
+ value: tuple[ProviderItem, ...],
45
+ ) -> tuple[ProviderItem, ...]:
46
+ return tuple(freeze_provider_item(item) for item in value)
40
47
 
41
48
  @field_serializer("items", when_used="json")
42
49
  def encode_bytes(
43
50
  self,
44
- items: tuple[Mapping[str, ProviderValue], ...],
51
+ items: tuple[ProviderItem, ...],
45
52
  ) -> tuple[dict[str, object], ...]:
46
53
  return tuple(
47
54
  {key: _encode_provider_bytes(value) for key, value in item.items()} for item in items
@@ -61,25 +68,22 @@ class ToolCallPart(BaseModel):
61
68
  type: Literal["tool_call"] = "tool_call"
62
69
  call_id: str
63
70
  name: str
64
- arguments: Mapping[str, JsonValue]
71
+ arguments: JsonObject
65
72
 
66
73
  @field_validator("arguments", mode="after")
67
74
  @classmethod
68
75
  def freeze_arguments(
69
76
  cls,
70
- value: Mapping[str, JsonValue],
71
- ) -> Mapping[str, JsonValue]:
72
- frozen = freeze_json(value)
73
- if not isinstance(frozen, Mapping):
74
- raise TypeError("arguments debe ser un objeto")
75
- return cast(Mapping[str, JsonValue], frozen)
77
+ value: JsonObject,
78
+ ) -> JsonObject:
79
+ return freeze_json_object(value)
76
80
 
77
81
  @field_serializer("arguments", when_used="json")
78
82
  def serialize_arguments(
79
83
  self,
80
- value: Mapping[str, JsonValue],
81
- ) -> dict[str, object]:
82
- return {key: thaw_json(cast(FrozenJsonValue, item)) for key, item in value.items()}
84
+ value: JsonObject,
85
+ ) -> dict[str, JsonValue]:
86
+ return thaw_json_object(value)
83
87
 
84
88
 
85
89
  class ToolResultPart(BaseModel):
@@ -93,7 +97,7 @@ class ToolResultPart(BaseModel):
93
97
  error_code: str | None = None
94
98
 
95
99
 
96
- Part = Annotated[TextPart | ToolCallPart | ToolResultPart, Field(discriminator="type")]
100
+ HistoryPart = Annotated[TextPart | ToolCallPart | ToolResultPart, Field(discriminator="type")]
97
101
 
98
102
 
99
103
  class Turn(BaseModel):
@@ -102,9 +106,24 @@ class Turn(BaseModel):
102
106
  model_config = ConfigDict(extra="forbid", frozen=True)
103
107
 
104
108
  role: Literal["user", "assistant", "tool"]
105
- parts: tuple[Part, ...]
109
+ parts: tuple[HistoryPart, ...]
106
110
  provider_state: ProviderState | None = None
107
111
 
112
+ @model_validator(mode="after")
113
+ def validate_role_contract(self) -> Self:
114
+ if self.role == "user":
115
+ valid_parts = all(isinstance(part, TextPart) for part in self.parts)
116
+ elif self.role == "assistant":
117
+ valid_parts = all(isinstance(part, TextPart | ToolCallPart) for part in self.parts)
118
+ else:
119
+ valid_parts = all(isinstance(part, ToolResultPart) for part in self.parts)
120
+
121
+ if not valid_parts:
122
+ raise ValueError(f"parts incompatibles con role={self.role}")
123
+ if self.role != "assistant" and self.provider_state is not None:
124
+ raise ValueError("provider_state requiere role=assistant")
125
+ return self
126
+
108
127
  @classmethod
109
128
  def user(cls, text: str) -> "Turn":
110
129
  return cls(role="user", parts=(TextPart(text=text),))
@@ -133,13 +152,3 @@ def _decode_provider_bytes(value: object) -> object:
133
152
  if set(value) == {_BYTES_TAG} and isinstance(value[_BYTES_TAG], str):
134
153
  return base64.b64decode(value[_BYTES_TAG], validate=True)
135
154
  return {key: _decode_provider_bytes(item) for key, item in value.items()}
136
-
137
-
138
- def _freeze_provider_item(
139
- value: Mapping[str, ProviderValue],
140
- ) -> Mapping[str, ProviderValue]:
141
- frozen = freeze_provider(value)
142
- if not isinstance(frozen, Mapping):
143
- raise TypeError("provider item debe ser un objeto")
144
- return frozen
145
-
@@ -1,4 +1,4 @@
1
- from collections.abc import Awaitable, Callable
1
+ from collections.abc import Awaitable, Callable, Sequence
2
2
  from dataclasses import dataclass
3
3
  from datetime import datetime
4
4
 
@@ -13,5 +13,4 @@ class TransformContext:
13
13
  session_id: str | None
14
14
 
15
15
 
16
- HistoryTransform = Callable[[TransformContext], Awaitable[tuple[Turn, ...] | list[Turn]]]
17
-
16
+ HistoryTransform = Callable[[TransformContext], Awaitable[Sequence[Turn]]]
@@ -0,0 +1,75 @@
1
+ from collections.abc import Mapping
2
+ from types import MappingProxyType
3
+ from typing import cast
4
+
5
+ from pydantic import JsonValue
6
+
7
+ type FrozenJsonValue = (
8
+ bool | int | float | str | tuple[FrozenJsonValue, ...] | Mapping[str, FrozenJsonValue] | None
9
+ )
10
+ type JsonInputObject = Mapping[str, JsonValue]
11
+ type FrozenJsonObject = Mapping[str, FrozenJsonValue]
12
+ type JsonObject = JsonInputObject | FrozenJsonObject
13
+ type ProviderValue = (
14
+ bool
15
+ | int
16
+ | float
17
+ | str
18
+ | bytes
19
+ | tuple[ProviderValue, ...]
20
+ | Mapping[str, ProviderValue]
21
+ | None
22
+ )
23
+ type ProviderItem = Mapping[str, ProviderValue]
24
+
25
+
26
+ def freeze_json_object(value: JsonObject) -> FrozenJsonObject:
27
+ frozen = {key: _freeze_json(item) for key, item in value.items()}
28
+ return MappingProxyType(frozen)
29
+
30
+
31
+ def thaw_json_object(value: JsonObject) -> dict[str, JsonValue]:
32
+ return {key: _thaw_json(item) for key, item in value.items()}
33
+
34
+
35
+ def freeze_provider_item(value: ProviderItem) -> ProviderItem:
36
+ frozen = {key: _freeze_provider(item) for key, item in value.items()}
37
+ return MappingProxyType(frozen)
38
+
39
+
40
+ def thaw_provider_item(value: ProviderItem) -> dict[str, object]:
41
+ return {key: _thaw_provider(item) for key, item in value.items()}
42
+
43
+
44
+ def _freeze_json(value: JsonValue | FrozenJsonValue) -> FrozenJsonValue:
45
+ if isinstance(value, list | tuple):
46
+ return tuple(_freeze_json(item) for item in value)
47
+ if isinstance(value, Mapping):
48
+ frozen = {key: _freeze_json(item) for key, item in value.items()}
49
+ return MappingProxyType(frozen)
50
+ return value
51
+
52
+
53
+ def _thaw_json(value: JsonValue | FrozenJsonValue) -> JsonValue:
54
+ if isinstance(value, list | tuple):
55
+ return [_thaw_json(item) for item in value]
56
+ if isinstance(value, Mapping):
57
+ return {key: _thaw_json(item) for key, item in value.items()}
58
+ return cast(JsonValue, value)
59
+
60
+
61
+ def _freeze_provider(value: ProviderValue) -> ProviderValue:
62
+ if isinstance(value, tuple):
63
+ return tuple(_freeze_provider(item) for item in value)
64
+ if isinstance(value, Mapping):
65
+ frozen = {key: _freeze_provider(item) for key, item in value.items()}
66
+ return MappingProxyType(frozen)
67
+ return value
68
+
69
+
70
+ def _thaw_provider(value: ProviderValue) -> object:
71
+ if isinstance(value, tuple):
72
+ return [_thaw_provider(item) for item in value]
73
+ if isinstance(value, Mapping):
74
+ return {key: _thaw_provider(item) for key, item in value.items()}
75
+ return value
@@ -1,2 +1,3 @@
1
1
  from cortex_agent_sdk.openai.engine import OpenAIEngine
2
+ from cortex_agent_sdk.openai.models import OpenAIModel, OpenAIModels
2
3
  from cortex_agent_sdk.openai.options import OpenAIOptions
@@ -11,24 +11,24 @@ from cortex_agent_sdk.engine import EngineRequest, EngineResult, ToolCall, Usage
11
11
  from cortex_agent_sdk.errores import AppError, CodigoError
12
12
  from cortex_agent_sdk.gateway import OpenAICompatibleGateway
13
13
  from cortex_agent_sdk.history.models import (
14
+ HistoryPart,
14
15
  ProviderState,
15
- ProviderValue,
16
16
  TextPart,
17
17
  ToolCallPart,
18
18
  ToolResultPart,
19
19
  Turn,
20
20
  )
21
21
  from cortex_agent_sdk.immutable import (
22
- FrozenJsonValue,
23
- FrozenProviderValue,
24
- thaw_json,
25
- thaw_provider,
22
+ ProviderItem,
23
+ thaw_json_object,
24
+ thaw_provider_item,
26
25
  )
26
+ from cortex_agent_sdk.openai.models import OpenAIModels
27
27
  from cortex_agent_sdk.openai.options import OpenAIOptions
28
28
  from cortex_agent_sdk.tools.models import ToolSpec
29
29
 
30
30
  _JSON_OBJECT = TypeAdapter(dict[str, JsonValue])
31
- _PROVIDER_OBJECT = TypeAdapter(dict[str, ProviderValue])
31
+ _PROVIDER_OBJECT = TypeAdapter(ProviderItem)
32
32
 
33
33
 
34
34
  class OpenAIEngine:
@@ -52,9 +52,13 @@ class OpenAIEngine:
52
52
 
53
53
  self._model = model
54
54
  self._options = options or OpenAIOptions()
55
- self._client = client or self._create_client(api_key, gateway)
55
+ if client is None:
56
+ self._client = self._create_client(api_key, gateway)
57
+ else:
58
+ self._client = client
56
59
  self._owns_client = client is None
57
60
  self._closed = False
61
+ self._models = OpenAIModels(self._client, self._ensure_open)
58
62
 
59
63
  @property
60
64
  def provider(self) -> str:
@@ -64,17 +68,19 @@ class OpenAIEngine:
64
68
  def model(self) -> str:
65
69
  return self._model
66
70
 
71
+ @property
72
+ def models(self) -> OpenAIModels:
73
+ return self._models
74
+
67
75
  async def __aenter__(self) -> Self:
68
- if self._closed:
69
- raise AppError(CodigoError.RECURSO_CERRADO, "OpenAIEngine cerrado")
76
+ self._ensure_open()
70
77
  return self
71
78
 
72
79
  async def __aexit__(self, *_: object) -> None:
73
80
  await self.aclose()
74
81
 
75
82
  async def generate(self, request: EngineRequest) -> EngineResult:
76
- if self._closed:
77
- raise AppError(CodigoError.RECURSO_CERRADO, "OpenAIEngine cerrado")
83
+ self._ensure_open()
78
84
 
79
85
  payload = cast(dict[str, Any], self._build_payload(request))
80
86
  try:
@@ -130,6 +136,10 @@ class OpenAIEngine:
130
136
  ) from error
131
137
  self._closed = True
132
138
 
139
+ def _ensure_open(self) -> None:
140
+ if self._closed:
141
+ raise AppError(CodigoError.RECURSO_CERRADO, "OpenAIEngine cerrado")
142
+
133
143
  def _create_client(
134
144
  self,
135
145
  api_key: str | None,
@@ -205,7 +215,7 @@ def _build_input(history: tuple[Turn, ...]) -> list[ResponseInputItemParam]:
205
215
  "historial contiene estado de otro provider",
206
216
  )
207
217
  items.extend(
208
- cast(ResponseInputItemParam, thaw_provider(cast(FrozenProviderValue, item)))
218
+ cast(ResponseInputItemParam, thaw_provider_item(item))
209
219
  for item in turn.provider_state.items
210
220
  )
211
221
  continue
@@ -226,9 +236,7 @@ def _build_input(history: tuple[Turn, ...]) -> list[ResponseInputItemParam]:
226
236
 
227
237
 
228
238
  def _tool_payload(spec: ToolSpec) -> dict[str, object]:
229
- parameters = {
230
- key: thaw_json(cast(FrozenJsonValue, value)) for key, value in spec.parameters.items()
231
- }
239
+ parameters = thaw_json_object(spec.parameters)
232
240
  return {
233
241
  "type": "function",
234
242
  "name": spec.name,
@@ -255,8 +263,8 @@ def _parse_tool_calls(output: list[object]) -> tuple[ToolCall, ...]:
255
263
  return tuple(calls)
256
264
 
257
265
 
258
- def _build_parts(text: str, calls: tuple[ToolCall, ...]):
259
- parts: list[TextPart | ToolCallPart] = []
266
+ def _build_parts(text: str, calls: tuple[ToolCall, ...]) -> tuple[HistoryPart, ...]:
267
+ parts: list[HistoryPart] = []
260
268
  if text:
261
269
  parts.append(TextPart(text=text))
262
270
  parts.extend(
@@ -0,0 +1,57 @@
1
+ from collections.abc import Callable
2
+ from typing import Literal
3
+
4
+ import openai
5
+ from openai import AsyncOpenAI
6
+ from pydantic import BaseModel, ConfigDict, TypeAdapter, ValidationError
7
+
8
+ from cortex_agent_sdk.errores import AppError, CodigoError
9
+
10
+
11
+ class OpenAIModel(BaseModel):
12
+ """Modelo disponible según el catálogo nativo de OpenAI."""
13
+
14
+ model_config = ConfigDict(extra="forbid", frozen=True)
15
+
16
+ id: str
17
+ created: int
18
+ object: Literal["model"]
19
+ owned_by: str
20
+
21
+
22
+ _OPENAI_MODELS = TypeAdapter(tuple[OpenAIModel, ...])
23
+
24
+
25
+ class OpenAIModels:
26
+ """Consulta el catálogo con el mismo cliente que usa el engine."""
27
+
28
+ def __init__(self, client: AsyncOpenAI, ensure_open: Callable[[], None]) -> None:
29
+ self._client = client
30
+ self._ensure_open = ensure_open
31
+
32
+ async def list(self) -> tuple[OpenAIModel, ...]:
33
+ self._ensure_open()
34
+ try:
35
+ response = await self._client.models.list()
36
+ except openai.APITimeoutError as error:
37
+ raise AppError(CodigoError.PROVIDER_TIMEOUT, "OpenAI agotó el timeout") from error
38
+ except openai.APIStatusError as error:
39
+ request_id = getattr(error, "request_id", None)
40
+ detail = f"OpenAI respondió {error.status_code}, request_id={request_id}"
41
+ raise AppError(CodigoError.PROVIDER_FALLO, detail) from error
42
+ except openai.APIConnectionError as error:
43
+ raise AppError(CodigoError.PROVIDER_FALLO, "falló la conexión con OpenAI") from error
44
+ except openai.APIError as error:
45
+ detail = f"OpenAI SDK lanzó {type(error).__name__}"
46
+ raise AppError(CodigoError.PROVIDER_FALLO, detail) from error
47
+
48
+ try:
49
+ return _OPENAI_MODELS.validate_python(
50
+ response.data,
51
+ from_attributes=True,
52
+ )
53
+ except ValidationError as error:
54
+ raise AppError(
55
+ CodigoError.PROVIDER_RESPUESTA_INVALIDA,
56
+ "OpenAI devolvió un catálogo de modelos inválido",
57
+ ) from error
@@ -2,6 +2,7 @@ import asyncio
2
2
  import time
3
3
  from collections.abc import AsyncIterator
4
4
  from contextlib import asynccontextmanager, suppress
5
+ from dataclasses import dataclass
5
6
  from datetime import UTC, datetime
6
7
  from hashlib import sha256
7
8
  from typing import Self, cast
@@ -19,6 +20,13 @@ from cortex_agent_sdk.sessions.store import SessionLease
19
20
  type _RedisArgument = str | bytes | int | float
20
21
 
21
22
 
23
+ @dataclass(frozen=True, slots=True)
24
+ class _RedisKeys:
25
+ lease: str
26
+ record: str
27
+ fence: str
28
+
29
+
22
30
  class _RedisLease:
23
31
  def __init__(
24
32
  self,
@@ -95,7 +103,10 @@ class RedisSessionStore:
95
103
  if not key_prefix or min(durations) <= 0:
96
104
  raise AppError(CodigoError.CONFIG_INVALIDA, "configuración de Redis inválida")
97
105
 
98
- self._client = client or Redis.from_url(cast(str, url), decode_responses=False)
106
+ if client is None:
107
+ self._client = Redis.from_url(cast(str, url), decode_responses=False)
108
+ else:
109
+ self._client = client
99
110
  self._owns_client = client is None
100
111
  self._key_prefix = key_prefix.rstrip(":")
101
112
  self._ttl_milliseconds = _milliseconds(ttl_seconds)
@@ -138,9 +149,10 @@ class RedisSessionStore:
138
149
 
139
150
  async def reset(self, session_id: str, timeout_seconds: float = 5.0) -> None:
140
151
  async with self.acquire(session_id, timeout_seconds) as lease:
152
+ keys = self._keys(session_id)
141
153
  result = await self._eval(
142
154
  scripts.RESET,
143
- self._keys(session_id)[:2],
155
+ (keys.lease, keys.record),
144
156
  self._lease_value(lease),
145
157
  )
146
158
  if int(cast(int, result)) != 1:
@@ -166,7 +178,7 @@ class RedisSessionStore:
166
178
  self._closed = True
167
179
 
168
180
  async def _acquire(self, session_id: str, owner: str, timeout_seconds: float) -> int:
169
- lease_key, _, fence_key = self._keys(session_id)
181
+ keys = self._keys(session_id)
170
182
  deadline = time.monotonic() + timeout_seconds
171
183
  while True:
172
184
  remaining = deadline - time.monotonic()
@@ -174,7 +186,7 @@ class RedisSessionStore:
174
186
  raise AppError(CodigoError.SESION_OCUPADA, f"sesión ocupada: {session_id}")
175
187
  result = await self._eval(
176
188
  scripts.ACQUIRE,
177
- (lease_key, fence_key),
189
+ (keys.lease, keys.fence),
178
190
  owner,
179
191
  self._lease_milliseconds,
180
192
  timeout_seconds=remaining,
@@ -188,16 +200,16 @@ class RedisSessionStore:
188
200
  await asyncio.sleep(min(self._retry_interval_seconds, remaining))
189
201
 
190
202
  async def _verify(self, session_id: str, lease_value: str) -> None:
191
- lease_key, _, _ = self._keys(session_id)
192
- result = await self._eval(scripts.VERIFY, (lease_key,), lease_value)
203
+ keys = self._keys(session_id)
204
+ result = await self._eval(scripts.VERIFY, (keys.lease,), lease_value)
193
205
  if int(cast(int, result)) != 1:
194
206
  raise AppError(CodigoError.SESION_LEASE_PERDIDO, "ownership de sesión perdido")
195
207
 
196
208
  async def _renew(self, session_id: str, lease_value: str) -> None:
197
- lease_key, _, _ = self._keys(session_id)
209
+ keys = self._keys(session_id)
198
210
  result = await self._eval(
199
211
  scripts.RENEW,
200
- (lease_key,),
212
+ (keys.lease,),
201
213
  lease_value,
202
214
  self._lease_milliseconds,
203
215
  )
@@ -205,10 +217,10 @@ class RedisSessionStore:
205
217
  raise AppError(CodigoError.SESION_LEASE_PERDIDO, "ownership de sesión perdido")
206
218
 
207
219
  async def _load(self, session_id: str, lease_value: str) -> SessionRecord | None:
208
- lease_key, session_key, _ = self._keys(session_id)
220
+ keys = self._keys(session_id)
209
221
  raw = await self._eval(
210
222
  scripts.LOAD,
211
- (lease_key, session_key),
223
+ (keys.lease, keys.record),
212
224
  lease_value,
213
225
  self._ttl_milliseconds,
214
226
  )
@@ -226,10 +238,10 @@ class RedisSessionStore:
226
238
  "updated_at": datetime.now(UTC),
227
239
  }
228
240
  )
229
- lease_key, session_key, _ = self._keys(record.session_id)
241
+ keys = self._keys(record.session_id)
230
242
  result = await self._eval(
231
243
  scripts.SAVE,
232
- (lease_key, session_key),
244
+ (keys.lease, keys.record),
233
245
  lease_value,
234
246
  record.version,
235
247
  saved.version,
@@ -244,8 +256,8 @@ class RedisSessionStore:
244
256
  raise AppError(CodigoError.SESION_LEASE_PERDIDO, "ownership de sesión perdido")
245
257
 
246
258
  async def _release(self, session_id: str, lease_value: str) -> None:
247
- lease_key, _, _ = self._keys(session_id)
248
- await self._eval(scripts.RELEASE, (lease_key,), lease_value)
259
+ keys = self._keys(session_id)
260
+ await self._eval(scripts.RELEASE, (keys.lease,), lease_value)
249
261
 
250
262
  async def _eval(
251
263
  self,
@@ -270,10 +282,14 @@ class RedisSessionStore:
270
282
  f"Redis lanzó {type(error).__name__}",
271
283
  ) from error
272
284
 
273
- def _keys(self, session_id: str) -> tuple[str, str, str]:
285
+ def _keys(self, session_id: str) -> _RedisKeys:
274
286
  digest = sha256(session_id.encode()).hexdigest()
275
287
  base = f"{self._key_prefix}:{{{digest}}}"
276
- return f"{base}:lease", f"{base}:record", f"{base}:fence"
288
+ return _RedisKeys(
289
+ lease=f"{base}:lease",
290
+ record=f"{base}:record",
291
+ fence=f"{base}:fence",
292
+ )
277
293
 
278
294
  def _lease_value(self, lease: SessionLease) -> str:
279
295
  if not isinstance(lease, _RedisLease):
@@ -290,4 +306,3 @@ def _milliseconds(seconds: float) -> int:
290
306
  if value < 1:
291
307
  raise AppError(CodigoError.CONFIG_INVALIDA, "duración Redis demasiado pequeña")
292
308
  return value
293
-
@@ -2,14 +2,14 @@ import inspect
2
2
  from collections.abc import Mapping
3
3
  from dataclasses import dataclass
4
4
  from types import MappingProxyType
5
- from typing import Annotated, Any, cast, get_args, get_origin, get_type_hints
5
+ from typing import Annotated, Any, get_args, get_origin, get_type_hints
6
6
 
7
7
  from jsonschema import Draft202012Validator
8
8
  from jsonschema.exceptions import SchemaError
9
9
  from pydantic import BaseModel, ConfigDict, JsonValue, create_model
10
10
 
11
11
  from cortex_agent_sdk.errores import AppError, CodigoError
12
- from cortex_agent_sdk.immutable import FrozenJsonValue, thaw_json
12
+ from cortex_agent_sdk.immutable import thaw_json_object
13
13
  from cortex_agent_sdk.tools.models import Injected, ToolBinding, ToolFunction, ToolSpec
14
14
 
15
15
 
@@ -148,5 +148,4 @@ def _explicit_validator(
148
148
 
149
149
 
150
150
  def _plain_schema(spec: ToolSpec) -> dict[str, JsonValue]:
151
- return {key: thaw_json(cast(FrozenJsonValue, value)) for key, value in spec.parameters.items()}
152
-
151
+ return thaw_json_object(spec.parameters)
@@ -3,14 +3,13 @@ import json
3
3
  from collections.abc import Iterable, Mapping
4
4
  from dataclasses import dataclass
5
5
  from types import MappingProxyType
6
- from typing import cast
7
6
 
8
7
  from pydantic import JsonValue, TypeAdapter, ValidationError
9
8
 
10
9
  from cortex_agent_sdk.engine import ToolCall
11
10
  from cortex_agent_sdk.errores import AppError, CodigoError
12
11
  from cortex_agent_sdk.history.models import ToolResultPart
13
- from cortex_agent_sdk.immutable import FrozenJsonValue, thaw_json
12
+ from cortex_agent_sdk.immutable import thaw_json_object
14
13
  from cortex_agent_sdk.tools.contracts import ToolDefinition, build_definition
15
14
  from cortex_agent_sdk.tools.models import ToolBinding, ToolFunction, ToolSpec
16
15
 
@@ -132,9 +131,7 @@ def _validate_arguments(
132
131
  definition: ToolDefinition,
133
132
  call: ToolCall,
134
133
  ) -> dict[str, object] | ToolOutcome:
135
- public_input = {
136
- key: thaw_json(cast(FrozenJsonValue, value)) for key, value in call.arguments.items()
137
- }
134
+ public_input = thaw_json_object(call.arguments)
138
135
  if definition.schema_validator is not None:
139
136
  schema_error = next(definition.schema_validator.iter_errors(public_input), None)
140
137
  if schema_error is not None:
@@ -178,4 +175,3 @@ def _failed_outcome(
178
175
  payload = {"ok": False, "error": {"code": code.value, "message": message}}
179
176
  output = json.dumps(payload, ensure_ascii=False, separators=(",", ":"))
180
177
  return ToolOutcome(call, output, True, is_final_answer, code.value)
181
-
@@ -1,10 +1,10 @@
1
1
  from collections.abc import Awaitable, Callable, Mapping
2
2
  from dataclasses import dataclass, field
3
- from typing import cast
3
+ from types import MappingProxyType
4
4
 
5
5
  from pydantic import BaseModel, ConfigDict, JsonValue, field_serializer, field_validator
6
6
 
7
- from cortex_agent_sdk.immutable import FrozenJsonValue, freeze_json, thaw_json
7
+ from cortex_agent_sdk.immutable import JsonObject, freeze_json_object, thaw_json_object
8
8
 
9
9
 
10
10
  class _InjectedMarker:
@@ -22,26 +22,23 @@ class ToolSpec(BaseModel):
22
22
 
23
23
  name: str
24
24
  description: str = ""
25
- parameters: Mapping[str, JsonValue]
25
+ parameters: JsonObject
26
26
  strict: bool = False
27
27
 
28
28
  @field_validator("parameters", mode="after")
29
29
  @classmethod
30
30
  def freeze_parameters(
31
31
  cls,
32
- value: Mapping[str, JsonValue],
33
- ) -> Mapping[str, JsonValue]:
34
- frozen = freeze_json(value)
35
- if not isinstance(frozen, Mapping):
36
- raise TypeError("parameters debe ser un objeto")
37
- return cast(Mapping[str, JsonValue], frozen)
32
+ value: JsonObject,
33
+ ) -> JsonObject:
34
+ return freeze_json_object(value)
38
35
 
39
36
  @field_serializer("parameters", when_used="json")
40
37
  def serialize_parameters(
41
38
  self,
42
- value: Mapping[str, JsonValue],
43
- ) -> dict[str, object]:
44
- return {key: thaw_json(cast(FrozenJsonValue, item)) for key, item in value.items()}
39
+ value: JsonObject,
40
+ ) -> dict[str, JsonValue]:
41
+ return thaw_json_object(value)
45
42
 
46
43
 
47
44
  ToolFunction = Callable[..., Awaitable[object]]
@@ -55,3 +52,6 @@ class ToolBinding:
55
52
  spec: ToolSpec | None = None
56
53
  private_arguments: Mapping[str, object] = field(default_factory=dict)
57
54
 
55
+ def __post_init__(self) -> None:
56
+ frozen_arguments = MappingProxyType(dict(self.private_arguments))
57
+ object.__setattr__(self, "private_arguments", frozen_arguments)
@@ -1 +0,0 @@
1
- """Espacio reservado para el engine de Google posterior a 0.0.1."""
@@ -1,54 +0,0 @@
1
- from collections.abc import Mapping
2
- from types import MappingProxyType
3
- from typing import cast
4
-
5
- from pydantic import JsonValue
6
-
7
- type FrozenJsonValue = (
8
- bool | int | float | str | tuple[FrozenJsonValue, ...] | Mapping[str, FrozenJsonValue] | None
9
- )
10
- type FrozenProviderValue = (
11
- bool
12
- | int
13
- | float
14
- | str
15
- | bytes
16
- | tuple[FrozenProviderValue, ...]
17
- | Mapping[str, FrozenProviderValue]
18
- | None
19
- )
20
-
21
-
22
- def freeze_json(value: JsonValue | Mapping[str, JsonValue]) -> FrozenJsonValue:
23
- if isinstance(value, list):
24
- return tuple(freeze_json(item) for item in value)
25
- if isinstance(value, Mapping):
26
- frozen = {key: freeze_json(item) for key, item in value.items()}
27
- return MappingProxyType(frozen)
28
- return value
29
-
30
-
31
- def thaw_json(value: FrozenJsonValue) -> JsonValue:
32
- if isinstance(value, tuple):
33
- return [thaw_json(item) for item in value]
34
- if isinstance(value, Mapping):
35
- return {key: thaw_json(item) for key, item in value.items()}
36
- return cast(JsonValue, value)
37
-
38
-
39
- def freeze_provider(value: FrozenProviderValue) -> FrozenProviderValue:
40
- if isinstance(value, tuple):
41
- return tuple(freeze_provider(item) for item in value)
42
- if isinstance(value, Mapping):
43
- frozen = {key: freeze_provider(item) for key, item in value.items()}
44
- return MappingProxyType(frozen)
45
- return value
46
-
47
-
48
- def thaw_provider(value: FrozenProviderValue) -> object:
49
- if isinstance(value, tuple):
50
- return [thaw_provider(item) for item in value]
51
- if isinstance(value, Mapping):
52
- return {key: thaw_provider(item) for key, item in value.items()}
53
- return value
54
-