python-codex 0.2.6__py3-none-any.whl → 0.2.7__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.
- pycodex/__init__.py +4 -0
- pycodex/agent.py +54 -14
- pycodex/cli.py +110 -36
- pycodex/image_utils.py +79 -0
- pycodex/model.py +7 -1
- pycodex/portable.py +14 -7
- pycodex/prompts/models.json +170 -0
- pycodex/runtime.py +2 -0
- pycodex/tools/__init__.py +3 -0
- pycodex/tools/clock_tool.py +168 -0
- pycodex/tools/view_image_tool.py +7 -9
- pycodex/utils/compactor.py +17 -2
- pycodex/utils/session_persist.py +50 -2
- pycodex/utils/visualize.py +11 -4
- {python_codex-0.2.6.dist-info → python_codex-0.2.7.dist-info}/METADATA +29 -15
- {python_codex-0.2.6.dist-info → python_codex-0.2.7.dist-info}/RECORD +27 -25
- responses_server/config.py +4 -1
- responses_server/messages_api.py +49 -0
- responses_server/payload_processors.py +1 -0
- responses_server/stream_router.py +108 -22
- responses_server/trajectory_dump.py +18 -2
- workspace_server/app.py +12 -12
- workspace_server/workspace.html +7 -4
- workspace_server/workspaces.py +34 -24
- {python_codex-0.2.6.dist-info → python_codex-0.2.7.dist-info}/WHEEL +0 -0
- {python_codex-0.2.6.dist-info → python_codex-0.2.7.dist-info}/entry_points.txt +0 -0
- {python_codex-0.2.6.dist-info → python_codex-0.2.7.dist-info}/licenses/LICENSE +0 -0
|
@@ -143,6 +143,17 @@ class StreamRouter:
|
|
|
143
143
|
),
|
|
144
144
|
"stream": True,
|
|
145
145
|
}
|
|
146
|
+
reasoning = incomming_request.get("reasoning")
|
|
147
|
+
if isinstance(reasoning, dict):
|
|
148
|
+
reasoning_effort = reasoning.get("effort")
|
|
149
|
+
if isinstance(reasoning_effort, str) and reasoning_effort:
|
|
150
|
+
# vLLM's Responses renderer consumes effort as a template
|
|
151
|
+
# kwarg; keeping it out of the chat schema also preserves
|
|
152
|
+
# provider-specific values such as `max`.
|
|
153
|
+
payload["chat_template_kwargs"] = {
|
|
154
|
+
"reasoning_effort": reasoning_effort,
|
|
155
|
+
}
|
|
156
|
+
|
|
146
157
|
max_tokens = self._coerce_positive_int(
|
|
147
158
|
incomming_request.get("max_output_tokens")
|
|
148
159
|
)
|
|
@@ -224,6 +235,11 @@ class StreamRouter:
|
|
|
224
235
|
raise OutcommingChatError(
|
|
225
236
|
f"outcomming chat request failed with status {exc.code}: {body[:500]}"
|
|
226
237
|
) from exc
|
|
238
|
+
except TimeoutError as exc:
|
|
239
|
+
raise OutcommingChatError(
|
|
240
|
+
"outcomming chat request timed out after "
|
|
241
|
+
f"{self._config.timeout_seconds:g}s"
|
|
242
|
+
) from exc
|
|
227
243
|
except urllib.error.URLError as exc:
|
|
228
244
|
raise OutcommingChatError(
|
|
229
245
|
f"outcomming chat request failed: {exc.reason}"
|
|
@@ -285,6 +301,11 @@ class StreamRouter:
|
|
|
285
301
|
raise OutcommingChatError(
|
|
286
302
|
f"outcomming messages request failed with status {exc.code}: {body[:500]}"
|
|
287
303
|
) from exc
|
|
304
|
+
except TimeoutError as exc:
|
|
305
|
+
raise OutcommingChatError(
|
|
306
|
+
"outcomming messages request timed out after "
|
|
307
|
+
f"{self._config.timeout_seconds:g}s"
|
|
308
|
+
) from exc
|
|
288
309
|
except urllib.error.URLError as exc:
|
|
289
310
|
raise OutcommingChatError(
|
|
290
311
|
f"outcomming messages request failed: {exc.reason}"
|
|
@@ -443,7 +464,10 @@ class StreamRouter:
|
|
|
443
464
|
outcomming_stream = self.open_outcomming_stream(outcomming_request)
|
|
444
465
|
if trajectory_dump is None:
|
|
445
466
|
return outcomming_stream
|
|
446
|
-
return trajectory_dump.wrap_stream(
|
|
467
|
+
return trajectory_dump.wrap_stream(
|
|
468
|
+
outcomming_stream,
|
|
469
|
+
outcomming_request,
|
|
470
|
+
)
|
|
447
471
|
|
|
448
472
|
def _responses_input_to_chat_messages(
|
|
449
473
|
self,
|
|
@@ -455,6 +479,7 @@ class StreamRouter:
|
|
|
455
479
|
messages.append({"role": "developer", "content": instructions})
|
|
456
480
|
|
|
457
481
|
pending_assistant: 'typing.Union[typing.Dict[str, object], None]' = None
|
|
482
|
+
pending_tool_images: 'typing.List[typing.Dict[str, object]]' = []
|
|
458
483
|
|
|
459
484
|
def flush_pending_assistant() -> 'None':
|
|
460
485
|
nonlocal pending_assistant
|
|
@@ -470,12 +495,20 @@ class StreamRouter:
|
|
|
470
495
|
messages.append(pending_assistant)
|
|
471
496
|
pending_assistant = None
|
|
472
497
|
|
|
498
|
+
def flush_pending_tool_images() -> 'None':
|
|
499
|
+
if not pending_tool_images:
|
|
500
|
+
return
|
|
501
|
+
messages.append({"role": "user", "content": list(pending_tool_images)})
|
|
502
|
+
del pending_tool_images[:]
|
|
503
|
+
|
|
473
504
|
for raw_item in input_items:
|
|
474
505
|
if not isinstance(raw_item, dict):
|
|
475
506
|
raise UnsupportedIncommingFeature(
|
|
476
507
|
"all incomming `input` items must be objects"
|
|
477
508
|
)
|
|
478
509
|
item_type = raw_item.get("type")
|
|
510
|
+
if item_type not in {"function_call_output", "custom_tool_call_output"}:
|
|
511
|
+
flush_pending_tool_images()
|
|
479
512
|
|
|
480
513
|
if item_type == "message":
|
|
481
514
|
role = str(raw_item.get("role", "")).strip()
|
|
@@ -483,8 +516,12 @@ class StreamRouter:
|
|
|
483
516
|
raise UnsupportedIncommingFeature(
|
|
484
517
|
f"unsupported incomming message role: {role or '<empty>'}"
|
|
485
518
|
)
|
|
486
|
-
text = self.
|
|
519
|
+
text, image_parts = self._split_content_parts(raw_item.get("content"))
|
|
487
520
|
if role == "assistant":
|
|
521
|
+
if image_parts:
|
|
522
|
+
raise UnsupportedIncommingFeature(
|
|
523
|
+
"assistant messages cannot carry `input_image` content parts"
|
|
524
|
+
)
|
|
488
525
|
if pending_assistant is None:
|
|
489
526
|
pending_assistant = {"role": "assistant"}
|
|
490
527
|
if text:
|
|
@@ -493,7 +530,13 @@ class StreamRouter:
|
|
|
493
530
|
)
|
|
494
531
|
continue
|
|
495
532
|
flush_pending_assistant()
|
|
496
|
-
|
|
533
|
+
if image_parts:
|
|
534
|
+
content: 'object' = (
|
|
535
|
+
([{"type": "text", "text": text}] if text else []) + image_parts
|
|
536
|
+
)
|
|
537
|
+
else:
|
|
538
|
+
content = text
|
|
539
|
+
messages.append({"role": role, "content": content})
|
|
497
540
|
continue
|
|
498
541
|
|
|
499
542
|
if item_type == "reasoning":
|
|
@@ -532,15 +575,17 @@ class StreamRouter:
|
|
|
532
575
|
|
|
533
576
|
if item_type == "function_call_output":
|
|
534
577
|
flush_pending_assistant()
|
|
578
|
+
text, image_parts = self._split_tool_output_parts(
|
|
579
|
+
raw_item.get("output")
|
|
580
|
+
)
|
|
535
581
|
messages.append(
|
|
536
582
|
{
|
|
537
583
|
"role": "tool",
|
|
538
584
|
"tool_call_id": str(raw_item.get("call_id", "")).strip(),
|
|
539
|
-
"content":
|
|
540
|
-
raw_item.get("output")
|
|
541
|
-
),
|
|
585
|
+
"content": text,
|
|
542
586
|
}
|
|
543
587
|
)
|
|
588
|
+
pending_tool_images.extend(image_parts)
|
|
544
589
|
continue
|
|
545
590
|
|
|
546
591
|
if item_type == "custom_tool_call":
|
|
@@ -559,15 +604,17 @@ class StreamRouter:
|
|
|
559
604
|
|
|
560
605
|
if item_type == "custom_tool_call_output":
|
|
561
606
|
flush_pending_assistant()
|
|
607
|
+
text, image_parts = self._split_tool_output_parts(
|
|
608
|
+
raw_item.get("output")
|
|
609
|
+
)
|
|
562
610
|
messages.append(
|
|
563
611
|
{
|
|
564
612
|
"role": "tool",
|
|
565
613
|
"tool_call_id": str(raw_item.get("call_id", "")).strip(),
|
|
566
|
-
"content":
|
|
567
|
-
raw_item.get("output")
|
|
568
|
-
),
|
|
614
|
+
"content": text,
|
|
569
615
|
}
|
|
570
616
|
)
|
|
617
|
+
pending_tool_images.extend(image_parts)
|
|
571
618
|
continue
|
|
572
619
|
|
|
573
620
|
raise UnsupportedIncommingFeature(
|
|
@@ -575,6 +622,7 @@ class StreamRouter:
|
|
|
575
622
|
)
|
|
576
623
|
|
|
577
624
|
flush_pending_assistant()
|
|
625
|
+
flush_pending_tool_images()
|
|
578
626
|
return messages
|
|
579
627
|
|
|
580
628
|
def _coerce_positive_int(self, raw_value: 'object') -> 'typing.Union[int, None]':
|
|
@@ -584,17 +632,21 @@ class StreamRouter:
|
|
|
584
632
|
return raw_value
|
|
585
633
|
return None
|
|
586
634
|
|
|
587
|
-
def
|
|
635
|
+
def _split_content_parts(
|
|
636
|
+
self,
|
|
637
|
+
raw_content: 'object',
|
|
638
|
+
) -> 'typing.Tuple[str, typing.List[typing.Dict[str, object]]]':
|
|
588
639
|
if raw_content is None:
|
|
589
|
-
return ""
|
|
640
|
+
return "", []
|
|
590
641
|
if isinstance(raw_content, str):
|
|
591
|
-
return raw_content
|
|
642
|
+
return raw_content, []
|
|
592
643
|
if not isinstance(raw_content, list):
|
|
593
644
|
raise UnsupportedIncommingFeature(
|
|
594
645
|
"message `content` must be a list or string"
|
|
595
646
|
)
|
|
596
647
|
|
|
597
648
|
text_parts: 'typing.List[str]' = []
|
|
649
|
+
image_parts: 'typing.List[typing.Dict[str, object]]' = []
|
|
598
650
|
for part in raw_content:
|
|
599
651
|
if not isinstance(part, dict):
|
|
600
652
|
raise UnsupportedIncommingFeature(
|
|
@@ -604,17 +656,38 @@ class StreamRouter:
|
|
|
604
656
|
if part_type in {"input_text", "output_text"}:
|
|
605
657
|
text_parts.append(str(part.get("text", "")))
|
|
606
658
|
continue
|
|
659
|
+
if part_type == "input_image":
|
|
660
|
+
image_parts.append(self._build_chat_image_part(part))
|
|
661
|
+
continue
|
|
607
662
|
raise UnsupportedIncommingFeature(
|
|
608
663
|
f"content part type `{part_type}` is not yet supported by the chat backend"
|
|
609
664
|
)
|
|
610
|
-
return "".join(text_parts)
|
|
665
|
+
return "".join(text_parts), image_parts
|
|
666
|
+
|
|
667
|
+
def _build_chat_image_part(
|
|
668
|
+
self,
|
|
669
|
+
part: 'typing.Dict[str, object]',
|
|
670
|
+
) -> 'typing.Dict[str, object]':
|
|
671
|
+
image_url = str(part.get("image_url", "") or "").strip()
|
|
672
|
+
if not image_url:
|
|
673
|
+
raise UnsupportedIncommingFeature(
|
|
674
|
+
"`input_image` content parts must carry a non-empty `image_url`"
|
|
675
|
+
)
|
|
676
|
+
image_payload: 'typing.Dict[str, object]' = {"url": image_url}
|
|
677
|
+
detail = part.get("detail")
|
|
678
|
+
if isinstance(detail, str) and detail in {"auto", "low", "high"}:
|
|
679
|
+
image_payload["detail"] = detail
|
|
680
|
+
return {"type": "image_url", "image_url": image_payload}
|
|
611
681
|
|
|
612
|
-
def
|
|
682
|
+
def _split_tool_output_parts(
|
|
683
|
+
self,
|
|
684
|
+
raw_output: 'object',
|
|
685
|
+
) -> 'typing.Tuple[str, typing.List[typing.Dict[str, object]]]':
|
|
613
686
|
if isinstance(raw_output, str):
|
|
614
|
-
return raw_output
|
|
687
|
+
return raw_output, []
|
|
615
688
|
if isinstance(raw_output, list):
|
|
616
|
-
return self.
|
|
617
|
-
return json.dumps(raw_output, ensure_ascii=False)
|
|
689
|
+
return self._split_content_parts(raw_output)
|
|
690
|
+
return json.dumps(raw_output, ensure_ascii=False), []
|
|
618
691
|
|
|
619
692
|
def _coalesce_reasoning_text(self, raw_item: 'typing.Dict[str, object]') -> 'str':
|
|
620
693
|
content = raw_item.get("content")
|
|
@@ -732,12 +805,20 @@ class StreamRouter:
|
|
|
732
805
|
continue
|
|
733
806
|
|
|
734
807
|
reasoning = delta.get("reasoning")
|
|
735
|
-
if isinstance(reasoning, str) and reasoning:
|
|
736
|
-
reasoning_parts.append(reasoning)
|
|
737
|
-
|
|
738
808
|
reasoning_content = delta.get("reasoning_content")
|
|
739
|
-
if
|
|
740
|
-
|
|
809
|
+
if (
|
|
810
|
+
isinstance(reasoning, str)
|
|
811
|
+
and reasoning
|
|
812
|
+
and isinstance(reasoning_content, str)
|
|
813
|
+
and reasoning == reasoning_content
|
|
814
|
+
):
|
|
815
|
+
# Some chat providers expose the same delta under both aliases.
|
|
816
|
+
reasoning_parts.append(reasoning)
|
|
817
|
+
else:
|
|
818
|
+
if isinstance(reasoning, str) and reasoning:
|
|
819
|
+
reasoning_parts.append(reasoning)
|
|
820
|
+
if isinstance(reasoning_content, str) and reasoning_content:
|
|
821
|
+
reasoning_parts.append(reasoning_content)
|
|
741
822
|
|
|
742
823
|
content = delta.get("content")
|
|
743
824
|
if isinstance(content, str) and content:
|
|
@@ -912,6 +993,11 @@ class StreamRouter:
|
|
|
912
993
|
raise OutcommingChatError(
|
|
913
994
|
f"outcomming request failed with status {exc.code}: {body[:500]}"
|
|
914
995
|
) from exc
|
|
996
|
+
except TimeoutError as exc:
|
|
997
|
+
raise OutcommingChatError(
|
|
998
|
+
"outcomming request timed out after "
|
|
999
|
+
f"{self._config.timeout_seconds:g}s"
|
|
1000
|
+
) from exc
|
|
915
1001
|
except urllib.error.URLError as exc:
|
|
916
1002
|
raise OutcommingChatError(
|
|
917
1003
|
f"outcomming request failed: {exc.reason}"
|
|
@@ -22,9 +22,17 @@ class TrajectoryDumpWriter:
|
|
|
22
22
|
return None
|
|
23
23
|
return cls(root_dir)
|
|
24
24
|
|
|
25
|
-
def wrap_stream(
|
|
25
|
+
def wrap_stream(
|
|
26
|
+
self,
|
|
27
|
+
outcomming_stream,
|
|
28
|
+
outcomming_request: 'typing.Dict[str, object]',
|
|
29
|
+
):
|
|
26
30
|
def iter_stream():
|
|
27
|
-
capture = _TrajectoryCapture(
|
|
31
|
+
capture = _TrajectoryCapture(
|
|
32
|
+
self,
|
|
33
|
+
time.time(),
|
|
34
|
+
outcomming_request,
|
|
35
|
+
)
|
|
28
36
|
try:
|
|
29
37
|
for chunk in outcomming_stream:
|
|
30
38
|
capture.observe_chunk(chunk)
|
|
@@ -48,16 +56,22 @@ class _TrajectoryCapture:
|
|
|
48
56
|
self,
|
|
49
57
|
writer: 'TrajectoryDumpWriter',
|
|
50
58
|
send_timestamp: 'float',
|
|
59
|
+
outcomming_request: 'typing.Dict[str, object]',
|
|
51
60
|
) -> 'None':
|
|
52
61
|
self._writer = writer
|
|
53
62
|
self._send_timestamp = float(send_timestamp)
|
|
63
|
+
self._outcomming_request = json.loads(json.dumps(outcomming_request))
|
|
54
64
|
self._prefill_token_ids = None
|
|
55
65
|
self._decode_token_ids = []
|
|
66
|
+
self._usage: 'typing.Dict[str, object]' = {}
|
|
56
67
|
self._closed = False
|
|
57
68
|
|
|
58
69
|
def observe_chunk(self, payload: 'object') -> 'None':
|
|
59
70
|
if not isinstance(payload, dict):
|
|
60
71
|
return
|
|
72
|
+
usage = payload.get("usage")
|
|
73
|
+
if isinstance(usage, dict) and usage:
|
|
74
|
+
self._usage = json.loads(json.dumps(usage))
|
|
61
75
|
if self._prefill_token_ids is None and "prompt_token_ids" in payload:
|
|
62
76
|
normalized_prefill = _normalize_token_ids(payload.get("prompt_token_ids"))
|
|
63
77
|
if normalized_prefill is not None:
|
|
@@ -78,6 +92,8 @@ class _TrajectoryCapture:
|
|
|
78
92
|
return
|
|
79
93
|
self._closed = True
|
|
80
94
|
record = {
|
|
95
|
+
"request": self._outcomming_request,
|
|
96
|
+
"usage": self._usage,
|
|
81
97
|
"tokens": {
|
|
82
98
|
"prefill": list(self._prefill_token_ids or []),
|
|
83
99
|
"decode": list(self._decode_token_ids),
|
workspace_server/app.py
CHANGED
|
@@ -32,7 +32,8 @@ from pycodex.utils.session_persist import (
|
|
|
32
32
|
)
|
|
33
33
|
from pycodex.utils import uuid7_string
|
|
34
34
|
from pycodex.utils.visualize import (
|
|
35
|
-
|
|
35
|
+
IDLE_SLEEPING_STATUS,
|
|
36
|
+
background_work_count,
|
|
36
37
|
percent_of_context_window_remaining,
|
|
37
38
|
shorten_title,
|
|
38
39
|
tool_summary,
|
|
@@ -93,6 +94,12 @@ def build_parser() -> "argparse.ArgumentParser":
|
|
|
93
94
|
default=None,
|
|
94
95
|
help="Optional base instructions override passed to the model.",
|
|
95
96
|
)
|
|
97
|
+
parser.add_argument(
|
|
98
|
+
"--toolset",
|
|
99
|
+
nargs="*",
|
|
100
|
+
default=None,
|
|
101
|
+
help="Builtin tool names for all sessions; an empty list disables tools.",
|
|
102
|
+
)
|
|
96
103
|
parser.add_argument(
|
|
97
104
|
"--timeout-seconds",
|
|
98
105
|
type=float,
|
|
@@ -520,12 +527,8 @@ class WebSessionView:
|
|
|
520
527
|
self._spinner_status = str(text or "").strip()
|
|
521
528
|
|
|
522
529
|
def _set_idle_spinner_status(self, payload: "typing.Dict[str, object]") -> None:
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
except (TypeError, ValueError):
|
|
526
|
-
background_work_count = 0
|
|
527
|
-
if background_work_count > 0:
|
|
528
|
-
self._set_spinner_status(IDLE_LISTENING_STATUS)
|
|
530
|
+
if background_work_count(payload) > 0:
|
|
531
|
+
self._set_spinner_status(IDLE_SLEEPING_STATUS)
|
|
529
532
|
else:
|
|
530
533
|
self._set_spinner_status("")
|
|
531
534
|
|
|
@@ -877,11 +880,7 @@ def create_multi_workspace_app(
|
|
|
877
880
|
entry = await registry.add_workspace(
|
|
878
881
|
str(payload.get("name") or ""),
|
|
879
882
|
work_dir=str(payload.get("dir") or "./"),
|
|
880
|
-
board=(
|
|
881
|
-
None
|
|
882
|
-
if payload.get("board") in (None, "")
|
|
883
|
-
else str(payload.get("board"))
|
|
884
|
-
),
|
|
883
|
+
board=payload.get("board"),
|
|
885
884
|
)
|
|
886
885
|
except ValueError as exc:
|
|
887
886
|
return JSONResponse({"ok": False, "error": str(exc)}, status_code=400)
|
|
@@ -1521,6 +1520,7 @@ def _build_workspace_entry(
|
|
|
1521
1520
|
else []
|
|
1522
1521
|
),
|
|
1523
1522
|
cwd=definition.work_dir,
|
|
1523
|
+
toolset=args.toolset,
|
|
1524
1524
|
)
|
|
1525
1525
|
return WorkspaceInteractiveSession(
|
|
1526
1526
|
build_cli_queue(agent),
|
workspace_server/workspace.html
CHANGED
|
@@ -304,7 +304,9 @@
|
|
|
304
304
|
height: 18px;
|
|
305
305
|
border: 0;
|
|
306
306
|
background: transparent;
|
|
307
|
-
opacity: 0
|
|
307
|
+
opacity: 0;
|
|
308
|
+
visibility: hidden;
|
|
309
|
+
pointer-events: none;
|
|
308
310
|
font-size: 17px;
|
|
309
311
|
font-weight: 500;
|
|
310
312
|
line-height: 18px;
|
|
@@ -314,12 +316,12 @@
|
|
|
314
316
|
font-weight: 400;
|
|
315
317
|
line-height: 31px;
|
|
316
318
|
}
|
|
317
|
-
.tab:hover .tab-close,
|
|
318
|
-
.tab:focus-within .tab-close,
|
|
319
319
|
.tab.active .tab-close {
|
|
320
320
|
opacity: 0.85;
|
|
321
|
+
visibility: visible;
|
|
322
|
+
pointer-events: auto;
|
|
321
323
|
}
|
|
322
|
-
.tab-close:disabled {
|
|
324
|
+
.tab.active .tab-close:disabled {
|
|
323
325
|
opacity: 0.22;
|
|
324
326
|
cursor: default;
|
|
325
327
|
}
|
|
@@ -1457,6 +1459,7 @@
|
|
|
1457
1459
|
if (!sessionId || sessionId === activeSessionId) return;
|
|
1458
1460
|
saveActiveSessionUiState();
|
|
1459
1461
|
activeSessionId = sessionId;
|
|
1462
|
+
renderTabs();
|
|
1460
1463
|
const state = stateForSession(sessionId);
|
|
1461
1464
|
state.lastRenderedSignature = "";
|
|
1462
1465
|
lastRenderedSignature = "";
|
workspace_server/workspaces.py
CHANGED
|
@@ -123,19 +123,23 @@ def load_workspace_definitions(
|
|
|
123
123
|
)
|
|
124
124
|
|
|
125
125
|
board_value = item.get("board")
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
"workspace
|
|
135
|
-
|
|
136
|
-
|
|
126
|
+
board_path = None
|
|
127
|
+
if board_value is not False:
|
|
128
|
+
if not isinstance(board_value, str) or not board_value.strip():
|
|
129
|
+
raise ValueError(
|
|
130
|
+
"workspace `{0}` requires a board path or false".format(workspace_id)
|
|
131
|
+
)
|
|
132
|
+
board_path = _resolve_workspace_path(board_value, path.parent)
|
|
133
|
+
if board_path in seen_boards:
|
|
134
|
+
raise ValueError("duplicate workspace board: {0}".format(board_path))
|
|
135
|
+
seen_boards.add(board_path)
|
|
136
|
+
if not board_path.parent.is_dir():
|
|
137
|
+
raise ValueError(
|
|
138
|
+
"workspace `{0}` board parent directory does not exist: {1}".format(
|
|
139
|
+
workspace_id,
|
|
140
|
+
board_path.parent,
|
|
141
|
+
)
|
|
137
142
|
)
|
|
138
|
-
)
|
|
139
143
|
|
|
140
144
|
result.append(
|
|
141
145
|
WorkspaceDefinition(
|
|
@@ -169,7 +173,7 @@ def save_workspace_definitions(
|
|
|
169
173
|
def _workspace_definition_to_json(
|
|
170
174
|
definition: 'WorkspaceDefinition',
|
|
171
175
|
base_dir: 'Path',
|
|
172
|
-
) -> 'typing.Dict[str,
|
|
176
|
+
) -> 'typing.Dict[str, object]':
|
|
173
177
|
result = {
|
|
174
178
|
"id": definition.workspace_id,
|
|
175
179
|
"work_dir": _format_path_for_workspace_config(definition.work_dir, base_dir),
|
|
@@ -179,6 +183,8 @@ def _workspace_definition_to_json(
|
|
|
179
183
|
definition.board_path,
|
|
180
184
|
base_dir,
|
|
181
185
|
)
|
|
186
|
+
else:
|
|
187
|
+
result["board"] = False
|
|
182
188
|
return result
|
|
183
189
|
|
|
184
190
|
|
|
@@ -440,7 +446,7 @@ class WorkspaceRegistry:
|
|
|
440
446
|
|
|
441
447
|
def _key_for_definition(self, definition: 'WorkspaceDefinition') -> str:
|
|
442
448
|
if definition.board_path is None:
|
|
443
|
-
|
|
449
|
+
return "workspace:{0}".format(definition.workspace_id)
|
|
444
450
|
return str(definition.board_path.resolve())
|
|
445
451
|
|
|
446
452
|
async def start(self) -> None:
|
|
@@ -466,7 +472,7 @@ class WorkspaceRegistry:
|
|
|
466
472
|
self,
|
|
467
473
|
name: str,
|
|
468
474
|
work_dir: str = "./",
|
|
469
|
-
board: "typing.Union[str, None]" = None,
|
|
475
|
+
board: "typing.Union[str, bool, None]" = None,
|
|
470
476
|
) -> 'WorkspaceEntry':
|
|
471
477
|
if self._entry_factory is None:
|
|
472
478
|
raise ValueError("workspace creation is unavailable")
|
|
@@ -475,15 +481,19 @@ class WorkspaceRegistry:
|
|
|
475
481
|
resolved_work_dir = _resolve_workspace_path(str(work_dir or "./"), base_dir)
|
|
476
482
|
resolved_work_dir.mkdir(parents=True, exist_ok=True)
|
|
477
483
|
|
|
478
|
-
board_path =
|
|
479
|
-
|
|
480
|
-
if
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
484
|
+
board_path = None
|
|
485
|
+
if board is not False:
|
|
486
|
+
if board is not None and not isinstance(board, str):
|
|
487
|
+
raise ValueError("board must be a path string or false")
|
|
488
|
+
board_path = (
|
|
489
|
+
_resolve_workspace_path(board, base_dir)
|
|
490
|
+
if board and board.strip()
|
|
491
|
+
else default_board_path()
|
|
492
|
+
)
|
|
493
|
+
board_path.parent.mkdir(parents=True, exist_ok=True)
|
|
494
|
+
board_key = str(board_path.resolve())
|
|
495
|
+
if board_key in self._entries:
|
|
496
|
+
raise ValueError("workspace board already exists: {0}".format(board_path))
|
|
487
497
|
|
|
488
498
|
workspace_id = normalize_workspace_id(name)
|
|
489
499
|
if not workspace_id:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|