codex-ipc 0.1.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.
- codex_app_server/__init__.py +112 -0
- codex_app_server/_inputs.py +63 -0
- codex_app_server/_message_router.py +158 -0
- codex_app_server/_run.py +112 -0
- codex_app_server/_version.py +37 -0
- codex_app_server/api.py +764 -0
- codex_app_server/async_client.py +214 -0
- codex_app_server/client.py +582 -0
- codex_app_server/errors.py +125 -0
- codex_app_server/generated/__init__.py +1 -0
- codex_app_server/generated/notification_registry.py +172 -0
- codex_app_server/generated/v2_all.py +8715 -0
- codex_app_server/models.py +99 -0
- codex_app_server/py.typed +0 -0
- codex_app_server/retry.py +41 -0
- codex_ipc/__init__.py +28 -0
- codex_ipc/codex/__init__.py +0 -0
- codex_ipc/codex/api.py +285 -0
- codex_ipc/codex/async_client.py +77 -0
- codex_ipc/codex/client.py +245 -0
- codex_ipc/codex/generated/__init__.py +0 -0
- codex_ipc/codex/generated/v2_all.py +83 -0
- codex_ipc/config.py +28 -0
- codex_ipc/session.py +509 -0
- codex_ipc/user_input.py +174 -0
- codex_ipc-0.1.0.dist-info/METADATA +62 -0
- codex_ipc-0.1.0.dist-info/RECORD +37 -0
- codex_ipc-0.1.0.dist-info/WHEEL +4 -0
- ipc/__init__.py +22 -0
- ipc/client.py +459 -0
- ipc/constants.py +55 -0
- ipc/conversation_state.py +846 -0
- ipc/frame_protocol.py +84 -0
- ipc/immer.py +207 -0
- ipc/owner_follower.py +2651 -0
- ipc/py.typed +0 -0
- ipc/router.py +555 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
from .async_client import AsyncAppServerClient
|
|
2
|
+
from .client import AppServerClient, AppServerConfig
|
|
3
|
+
from .errors import (
|
|
4
|
+
AppServerError,
|
|
5
|
+
AppServerRpcError,
|
|
6
|
+
InternalRpcError,
|
|
7
|
+
InvalidParamsError,
|
|
8
|
+
InvalidRequestError,
|
|
9
|
+
JsonRpcError,
|
|
10
|
+
MethodNotFoundError,
|
|
11
|
+
ParseError,
|
|
12
|
+
RetryLimitExceededError,
|
|
13
|
+
ServerBusyError,
|
|
14
|
+
TransportClosedError,
|
|
15
|
+
is_retryable_error,
|
|
16
|
+
)
|
|
17
|
+
from .generated.v2_all import (
|
|
18
|
+
AskForApproval,
|
|
19
|
+
Personality,
|
|
20
|
+
PlanType,
|
|
21
|
+
ReasoningEffort,
|
|
22
|
+
ReasoningSummary,
|
|
23
|
+
SandboxMode,
|
|
24
|
+
SandboxPolicy,
|
|
25
|
+
ServiceTier,
|
|
26
|
+
ThreadItem,
|
|
27
|
+
ThreadForkParams,
|
|
28
|
+
ThreadListParams,
|
|
29
|
+
ThreadResumeParams,
|
|
30
|
+
ThreadSortKey,
|
|
31
|
+
ThreadSourceKind,
|
|
32
|
+
ThreadStartParams,
|
|
33
|
+
ThreadTokenUsageUpdatedNotification,
|
|
34
|
+
TurnCompletedNotification,
|
|
35
|
+
TurnStartParams,
|
|
36
|
+
TurnStatus,
|
|
37
|
+
TurnSteerParams,
|
|
38
|
+
)
|
|
39
|
+
from .models import InitializeResponse
|
|
40
|
+
from .api import (
|
|
41
|
+
AsyncCodex,
|
|
42
|
+
AsyncThread,
|
|
43
|
+
AsyncTurnHandle,
|
|
44
|
+
Codex,
|
|
45
|
+
ImageInput,
|
|
46
|
+
Input,
|
|
47
|
+
InputItem,
|
|
48
|
+
LocalImageInput,
|
|
49
|
+
MentionInput,
|
|
50
|
+
RunResult,
|
|
51
|
+
SkillInput,
|
|
52
|
+
TextInput,
|
|
53
|
+
Thread,
|
|
54
|
+
TurnHandle,
|
|
55
|
+
)
|
|
56
|
+
from .retry import retry_on_overload
|
|
57
|
+
from ._version import __version__
|
|
58
|
+
|
|
59
|
+
__all__ = [
|
|
60
|
+
"__version__",
|
|
61
|
+
"AppServerClient",
|
|
62
|
+
"AsyncAppServerClient",
|
|
63
|
+
"AppServerConfig",
|
|
64
|
+
"Codex",
|
|
65
|
+
"AsyncCodex",
|
|
66
|
+
"Thread",
|
|
67
|
+
"AsyncThread",
|
|
68
|
+
"TurnHandle",
|
|
69
|
+
"AsyncTurnHandle",
|
|
70
|
+
"InitializeResponse",
|
|
71
|
+
"RunResult",
|
|
72
|
+
"Input",
|
|
73
|
+
"InputItem",
|
|
74
|
+
"TextInput",
|
|
75
|
+
"ImageInput",
|
|
76
|
+
"LocalImageInput",
|
|
77
|
+
"SkillInput",
|
|
78
|
+
"MentionInput",
|
|
79
|
+
"ThreadItem",
|
|
80
|
+
"ThreadTokenUsageUpdatedNotification",
|
|
81
|
+
"TurnCompletedNotification",
|
|
82
|
+
"AskForApproval",
|
|
83
|
+
"Personality",
|
|
84
|
+
"PlanType",
|
|
85
|
+
"ReasoningEffort",
|
|
86
|
+
"ReasoningSummary",
|
|
87
|
+
"SandboxMode",
|
|
88
|
+
"SandboxPolicy",
|
|
89
|
+
"ServiceTier",
|
|
90
|
+
"ThreadStartParams",
|
|
91
|
+
"ThreadResumeParams",
|
|
92
|
+
"ThreadListParams",
|
|
93
|
+
"ThreadSortKey",
|
|
94
|
+
"ThreadSourceKind",
|
|
95
|
+
"ThreadForkParams",
|
|
96
|
+
"TurnStatus",
|
|
97
|
+
"TurnStartParams",
|
|
98
|
+
"TurnSteerParams",
|
|
99
|
+
"retry_on_overload",
|
|
100
|
+
"AppServerError",
|
|
101
|
+
"TransportClosedError",
|
|
102
|
+
"JsonRpcError",
|
|
103
|
+
"AppServerRpcError",
|
|
104
|
+
"ParseError",
|
|
105
|
+
"InvalidRequestError",
|
|
106
|
+
"MethodNotFoundError",
|
|
107
|
+
"InvalidParamsError",
|
|
108
|
+
"InternalRpcError",
|
|
109
|
+
"ServerBusyError",
|
|
110
|
+
"RetryLimitExceededError",
|
|
111
|
+
"is_retryable_error",
|
|
112
|
+
]
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from .models import JsonObject
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(slots=True)
|
|
9
|
+
class TextInput:
|
|
10
|
+
text: str
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(slots=True)
|
|
14
|
+
class ImageInput:
|
|
15
|
+
url: str
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(slots=True)
|
|
19
|
+
class LocalImageInput:
|
|
20
|
+
path: str
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(slots=True)
|
|
24
|
+
class SkillInput:
|
|
25
|
+
name: str
|
|
26
|
+
path: str
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(slots=True)
|
|
30
|
+
class MentionInput:
|
|
31
|
+
name: str
|
|
32
|
+
path: str
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
InputItem = TextInput | ImageInput | LocalImageInput | SkillInput | MentionInput
|
|
36
|
+
Input = list[InputItem] | InputItem
|
|
37
|
+
RunInput = Input | str
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _to_wire_item(item: InputItem) -> JsonObject:
|
|
41
|
+
if isinstance(item, TextInput):
|
|
42
|
+
return {"type": "text", "text": item.text}
|
|
43
|
+
if isinstance(item, ImageInput):
|
|
44
|
+
return {"type": "image", "url": item.url}
|
|
45
|
+
if isinstance(item, LocalImageInput):
|
|
46
|
+
return {"type": "localImage", "path": item.path}
|
|
47
|
+
if isinstance(item, SkillInput):
|
|
48
|
+
return {"type": "skill", "name": item.name, "path": item.path}
|
|
49
|
+
if isinstance(item, MentionInput):
|
|
50
|
+
return {"type": "mention", "name": item.name, "path": item.path}
|
|
51
|
+
raise TypeError(f"unsupported input item: {type(item)!r}")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _to_wire_input(input: Input) -> list[JsonObject]:
|
|
55
|
+
if isinstance(input, list):
|
|
56
|
+
return [_to_wire_item(i) for i in input]
|
|
57
|
+
return [_to_wire_item(input)]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _normalize_run_input(input: RunInput) -> Input:
|
|
61
|
+
if isinstance(input, str):
|
|
62
|
+
return TextInput(input)
|
|
63
|
+
return input
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import queue
|
|
4
|
+
import threading
|
|
5
|
+
from collections import deque
|
|
6
|
+
|
|
7
|
+
from .errors import AppServerError, map_jsonrpc_error
|
|
8
|
+
from .generated.notification_registry import notification_turn_id
|
|
9
|
+
from .models import JsonValue, Notification, UnknownNotification
|
|
10
|
+
|
|
11
|
+
ResponseQueueItem = JsonValue | BaseException
|
|
12
|
+
NotificationQueueItem = Notification | BaseException
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class MessageRouter:
|
|
16
|
+
"""Route reader-thread messages to the SDK operation waiting for them.
|
|
17
|
+
|
|
18
|
+
The app-server stdio transport is a single ordered stream, so only the
|
|
19
|
+
reader thread should consume stdout. This router keeps the rest of the SDK
|
|
20
|
+
from competing for that stream by giving each in-flight JSON-RPC request
|
|
21
|
+
and active turn stream its own queue.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self) -> None:
|
|
25
|
+
self._lock = threading.Lock()
|
|
26
|
+
self._response_waiters: dict[str, queue.Queue[ResponseQueueItem]] = {}
|
|
27
|
+
self._turn_notifications: dict[str, queue.Queue[NotificationQueueItem]] = {}
|
|
28
|
+
self._pending_turn_notifications: dict[str, deque[Notification]] = {}
|
|
29
|
+
self._global_notifications: queue.Queue[NotificationQueueItem] = queue.Queue()
|
|
30
|
+
|
|
31
|
+
def create_response_waiter(self, request_id: str) -> queue.Queue[ResponseQueueItem]:
|
|
32
|
+
"""Register a one-shot queue for a JSON-RPC response id."""
|
|
33
|
+
|
|
34
|
+
waiter: queue.Queue[ResponseQueueItem] = queue.Queue(maxsize=1)
|
|
35
|
+
with self._lock:
|
|
36
|
+
self._response_waiters[request_id] = waiter
|
|
37
|
+
return waiter
|
|
38
|
+
|
|
39
|
+
def discard_response_waiter(self, request_id: str) -> None:
|
|
40
|
+
"""Remove a response waiter when the request could not be written."""
|
|
41
|
+
|
|
42
|
+
with self._lock:
|
|
43
|
+
self._response_waiters.pop(request_id, None)
|
|
44
|
+
|
|
45
|
+
def next_global_notification(self) -> Notification:
|
|
46
|
+
"""Block until the next notification that is not scoped to a turn."""
|
|
47
|
+
|
|
48
|
+
item = self._global_notifications.get()
|
|
49
|
+
if isinstance(item, BaseException):
|
|
50
|
+
raise item
|
|
51
|
+
return item
|
|
52
|
+
|
|
53
|
+
def register_turn(self, turn_id: str) -> None:
|
|
54
|
+
"""Register a queue for a turn stream and replay early events."""
|
|
55
|
+
|
|
56
|
+
turn_queue: queue.Queue[NotificationQueueItem] = queue.Queue()
|
|
57
|
+
with self._lock:
|
|
58
|
+
if turn_id in self._turn_notifications:
|
|
59
|
+
return
|
|
60
|
+
# A turn can emit events immediately after turn/start, before the
|
|
61
|
+
# caller receives the TurnHandle and starts streaming.
|
|
62
|
+
pending = self._pending_turn_notifications.pop(turn_id, deque())
|
|
63
|
+
self._turn_notifications[turn_id] = turn_queue
|
|
64
|
+
for notification in pending:
|
|
65
|
+
turn_queue.put(notification)
|
|
66
|
+
|
|
67
|
+
def unregister_turn(self, turn_id: str) -> None:
|
|
68
|
+
"""Stop routing future turn events to the stream queue."""
|
|
69
|
+
|
|
70
|
+
with self._lock:
|
|
71
|
+
self._turn_notifications.pop(turn_id, None)
|
|
72
|
+
|
|
73
|
+
def next_turn_notification(self, turn_id: str) -> Notification:
|
|
74
|
+
"""Block until the next notification for a registered turn."""
|
|
75
|
+
|
|
76
|
+
with self._lock:
|
|
77
|
+
turn_queue = self._turn_notifications.get(turn_id)
|
|
78
|
+
if turn_queue is None:
|
|
79
|
+
raise RuntimeError(f"turn {turn_id!r} is not registered for streaming")
|
|
80
|
+
item = turn_queue.get()
|
|
81
|
+
if isinstance(item, BaseException):
|
|
82
|
+
raise item
|
|
83
|
+
return item
|
|
84
|
+
|
|
85
|
+
def route_response(self, msg: dict[str, JsonValue]) -> None:
|
|
86
|
+
"""Deliver a JSON-RPC response or error to its request waiter."""
|
|
87
|
+
|
|
88
|
+
request_id = msg.get("id")
|
|
89
|
+
with self._lock:
|
|
90
|
+
waiter = self._response_waiters.pop(str(request_id), None)
|
|
91
|
+
if waiter is None:
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
if "error" in msg:
|
|
95
|
+
err = msg["error"]
|
|
96
|
+
if isinstance(err, dict):
|
|
97
|
+
waiter.put(
|
|
98
|
+
map_jsonrpc_error(
|
|
99
|
+
int(err.get("code", -32000)),
|
|
100
|
+
str(err.get("message", "unknown")),
|
|
101
|
+
err.get("data"),
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
else:
|
|
105
|
+
waiter.put(AppServerError("Malformed JSON-RPC error response"))
|
|
106
|
+
return
|
|
107
|
+
|
|
108
|
+
waiter.put(msg.get("result"))
|
|
109
|
+
|
|
110
|
+
def route_notification(self, notification: Notification) -> None:
|
|
111
|
+
"""Deliver a notification to a turn queue or the global queue."""
|
|
112
|
+
|
|
113
|
+
turn_id = self._notification_turn_id(notification)
|
|
114
|
+
if turn_id is None:
|
|
115
|
+
self._global_notifications.put(notification)
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
with self._lock:
|
|
119
|
+
turn_queue = self._turn_notifications.get(turn_id)
|
|
120
|
+
if turn_queue is None:
|
|
121
|
+
if notification.method == "turn/completed":
|
|
122
|
+
self._pending_turn_notifications.pop(turn_id, None)
|
|
123
|
+
return
|
|
124
|
+
self._pending_turn_notifications.setdefault(turn_id, deque()).append(
|
|
125
|
+
notification
|
|
126
|
+
)
|
|
127
|
+
return
|
|
128
|
+
turn_queue.put(notification)
|
|
129
|
+
|
|
130
|
+
def fail_all(self, exc: BaseException) -> None:
|
|
131
|
+
"""Wake every blocked waiter when the reader thread exits."""
|
|
132
|
+
|
|
133
|
+
with self._lock:
|
|
134
|
+
response_waiters = list(self._response_waiters.values())
|
|
135
|
+
self._response_waiters.clear()
|
|
136
|
+
turn_queues = list(self._turn_notifications.values())
|
|
137
|
+
self._pending_turn_notifications.clear()
|
|
138
|
+
# Put the same transport failure into every queue so no SDK call blocks
|
|
139
|
+
# forever waiting for a response that cannot arrive.
|
|
140
|
+
for waiter in response_waiters:
|
|
141
|
+
waiter.put(exc)
|
|
142
|
+
for turn_queue in turn_queues:
|
|
143
|
+
turn_queue.put(exc)
|
|
144
|
+
self._global_notifications.put(exc)
|
|
145
|
+
|
|
146
|
+
def _notification_turn_id(self, notification: Notification) -> str | None:
|
|
147
|
+
payload = notification.payload
|
|
148
|
+
if isinstance(payload, UnknownNotification):
|
|
149
|
+
raw_turn_id = payload.params.get("turnId")
|
|
150
|
+
if isinstance(raw_turn_id, str):
|
|
151
|
+
return raw_turn_id
|
|
152
|
+
raw_turn = payload.params.get("turn")
|
|
153
|
+
if isinstance(raw_turn, dict):
|
|
154
|
+
raw_nested_turn_id = raw_turn.get("id")
|
|
155
|
+
if isinstance(raw_nested_turn_id, str):
|
|
156
|
+
return raw_nested_turn_id
|
|
157
|
+
return None
|
|
158
|
+
return notification_turn_id(payload)
|
codex_app_server/_run.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import AsyncIterator, Iterator
|
|
5
|
+
|
|
6
|
+
from .generated.v2_all import (
|
|
7
|
+
AgentMessageThreadItem,
|
|
8
|
+
ItemCompletedNotification,
|
|
9
|
+
MessagePhase,
|
|
10
|
+
ThreadItem,
|
|
11
|
+
ThreadTokenUsage,
|
|
12
|
+
ThreadTokenUsageUpdatedNotification,
|
|
13
|
+
Turn as AppServerTurn,
|
|
14
|
+
TurnCompletedNotification,
|
|
15
|
+
TurnStatus,
|
|
16
|
+
)
|
|
17
|
+
from .models import Notification
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(slots=True)
|
|
21
|
+
class RunResult:
|
|
22
|
+
final_response: str | None
|
|
23
|
+
items: list[ThreadItem]
|
|
24
|
+
usage: ThreadTokenUsage | None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _agent_message_item_from_thread_item(
|
|
28
|
+
item: ThreadItem,
|
|
29
|
+
) -> AgentMessageThreadItem | None:
|
|
30
|
+
thread_item = item.root if hasattr(item, "root") else item
|
|
31
|
+
if isinstance(thread_item, AgentMessageThreadItem):
|
|
32
|
+
return thread_item
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _final_assistant_response_from_items(items: list[ThreadItem]) -> str | None:
|
|
37
|
+
last_unknown_phase_response: str | None = None
|
|
38
|
+
|
|
39
|
+
for item in reversed(items):
|
|
40
|
+
agent_message = _agent_message_item_from_thread_item(item)
|
|
41
|
+
if agent_message is None:
|
|
42
|
+
continue
|
|
43
|
+
if agent_message.phase == MessagePhase.final_answer:
|
|
44
|
+
return agent_message.text
|
|
45
|
+
if agent_message.phase is None and last_unknown_phase_response is None:
|
|
46
|
+
last_unknown_phase_response = agent_message.text
|
|
47
|
+
|
|
48
|
+
return last_unknown_phase_response
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _raise_for_failed_turn(turn: AppServerTurn) -> None:
|
|
52
|
+
if turn.status != TurnStatus.failed:
|
|
53
|
+
return
|
|
54
|
+
if turn.error is not None and turn.error.message:
|
|
55
|
+
raise RuntimeError(turn.error.message)
|
|
56
|
+
raise RuntimeError(f"turn failed with status {turn.status.value}")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _collect_run_result(stream: Iterator[Notification], *, turn_id: str) -> RunResult:
|
|
60
|
+
completed: TurnCompletedNotification | None = None
|
|
61
|
+
items: list[ThreadItem] = []
|
|
62
|
+
usage: ThreadTokenUsage | None = None
|
|
63
|
+
|
|
64
|
+
for event in stream:
|
|
65
|
+
payload = event.payload
|
|
66
|
+
if isinstance(payload, ItemCompletedNotification) and payload.turn_id == turn_id:
|
|
67
|
+
items.append(payload.item)
|
|
68
|
+
continue
|
|
69
|
+
if isinstance(payload, ThreadTokenUsageUpdatedNotification) and payload.turn_id == turn_id:
|
|
70
|
+
usage = payload.token_usage
|
|
71
|
+
continue
|
|
72
|
+
if isinstance(payload, TurnCompletedNotification) and payload.turn.id == turn_id:
|
|
73
|
+
completed = payload
|
|
74
|
+
|
|
75
|
+
if completed is None:
|
|
76
|
+
raise RuntimeError("turn completed event not received")
|
|
77
|
+
|
|
78
|
+
_raise_for_failed_turn(completed.turn)
|
|
79
|
+
return RunResult(
|
|
80
|
+
final_response=_final_assistant_response_from_items(items),
|
|
81
|
+
items=items,
|
|
82
|
+
usage=usage,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
async def _collect_async_run_result(
|
|
87
|
+
stream: AsyncIterator[Notification], *, turn_id: str
|
|
88
|
+
) -> RunResult:
|
|
89
|
+
completed: TurnCompletedNotification | None = None
|
|
90
|
+
items: list[ThreadItem] = []
|
|
91
|
+
usage: ThreadTokenUsage | None = None
|
|
92
|
+
|
|
93
|
+
async for event in stream:
|
|
94
|
+
payload = event.payload
|
|
95
|
+
if isinstance(payload, ItemCompletedNotification) and payload.turn_id == turn_id:
|
|
96
|
+
items.append(payload.item)
|
|
97
|
+
continue
|
|
98
|
+
if isinstance(payload, ThreadTokenUsageUpdatedNotification) and payload.turn_id == turn_id:
|
|
99
|
+
usage = payload.token_usage
|
|
100
|
+
continue
|
|
101
|
+
if isinstance(payload, TurnCompletedNotification) and payload.turn.id == turn_id:
|
|
102
|
+
completed = payload
|
|
103
|
+
|
|
104
|
+
if completed is None:
|
|
105
|
+
raise RuntimeError("turn completed event not received")
|
|
106
|
+
|
|
107
|
+
_raise_for_failed_turn(completed.turn)
|
|
108
|
+
return RunResult(
|
|
109
|
+
final_response=_final_assistant_response_from_items(items),
|
|
110
|
+
items=items,
|
|
111
|
+
usage=usage,
|
|
112
|
+
)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from importlib.metadata import PackageNotFoundError
|
|
5
|
+
from importlib.metadata import version as distribution_version
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
DISTRIBUTION_NAME = "openai-codex-app-server-sdk"
|
|
9
|
+
UNKNOWN_VERSION = "0+unknown"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def package_version() -> str:
|
|
13
|
+
source_version = _source_tree_project_version()
|
|
14
|
+
if source_version is not None:
|
|
15
|
+
return source_version
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
return distribution_version(DISTRIBUTION_NAME)
|
|
19
|
+
except PackageNotFoundError:
|
|
20
|
+
return UNKNOWN_VERSION
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _source_tree_project_version() -> str | None:
|
|
24
|
+
pyproject_path = Path(__file__).resolve().parents[2] / "pyproject.toml"
|
|
25
|
+
if not pyproject_path.exists():
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
match = re.search(
|
|
29
|
+
r'(?m)^version = "([^"]+)"$',
|
|
30
|
+
pyproject_path.read_text(encoding="utf-8"),
|
|
31
|
+
)
|
|
32
|
+
if match is None:
|
|
33
|
+
return None
|
|
34
|
+
return match.group(1)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
__version__ = package_version()
|