python-codex 0.2.7__py3-none-any.whl → 0.3.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 (84) hide show
  1. pycodex/__init__.py +14 -14
  2. pycodex/agent.py +465 -499
  3. pycodex/bootstrap.py +417 -0
  4. pycodex/cli.py +236 -510
  5. pycodex/compat.py +19 -5
  6. pycodex/context.py +222 -212
  7. pycodex/doctor.py +52 -48
  8. pycodex/events.py +857 -0
  9. pycodex/feishu_card.py +217 -163
  10. pycodex/feishu_link.py +43 -83
  11. pycodex/model.py +324 -253
  12. pycodex/model_metadata.py +19 -7
  13. pycodex/portable.py +76 -45
  14. pycodex/portable_server.py +32 -24
  15. pycodex/prompts/models.json +245 -983
  16. pycodex/protocol.py +177 -137
  17. pycodex/runtime.py +579 -176
  18. pycodex/runtime_services.py +204 -157
  19. pycodex/tools/__init__.py +1 -1
  20. pycodex/tools/apply_patch_tool.py +69 -48
  21. pycodex/tools/base_tool.py +89 -42
  22. pycodex/tools/clock_tool.py +58 -25
  23. pycodex/tools/close_agent_tool.py +2 -2
  24. pycodex/tools/code_mode_manager.py +77 -64
  25. pycodex/tools/exec_command_tool.py +26 -11
  26. pycodex/tools/exec_tool.py +4 -4
  27. pycodex/tools/grep_files_tool.py +12 -10
  28. pycodex/tools/ipython_tool.py +10 -13
  29. pycodex/tools/list_dir_tool.py +13 -9
  30. pycodex/tools/read_file_tool.py +29 -17
  31. pycodex/tools/request_permissions_tool.py +15 -5
  32. pycodex/tools/request_user_input_tool.py +13 -104
  33. pycodex/tools/resume_agent_tool.py +2 -2
  34. pycodex/tools/send_input_tool.py +11 -8
  35. pycodex/tools/shell_command_tool.py +7 -5
  36. pycodex/tools/shell_tool.py +7 -5
  37. pycodex/tools/spawn_agent_tool.py +7 -4
  38. pycodex/tools/unified_exec_manager.py +102 -69
  39. pycodex/tools/update_plan_tool.py +8 -5
  40. pycodex/tools/view_image_tool.py +7 -5
  41. pycodex/tools/wait_agent_tool.py +27 -4
  42. pycodex/tools/wait_tool.py +5 -4
  43. pycodex/tools/web_search_tool.py +4 -2
  44. pycodex/tools/write_stdin_tool.py +12 -11
  45. pycodex/utils/__init__.py +2 -17
  46. pycodex/utils/compactor.py +41 -72
  47. pycodex/utils/debug.py +2 -2
  48. pycodex/utils/dotenv.py +6 -7
  49. pycodex/utils/event_helpers.py +190 -0
  50. pycodex/utils/get_env.py +27 -70
  51. pycodex/{image_utils.py → utils/image_utils.py} +8 -11
  52. pycodex/utils/random_ids.py +1 -2
  53. pycodex/utils/session_persist.py +217 -163
  54. pycodex/utils/truncation.py +21 -45
  55. python_codex-0.3.0.dist-info/METADATA +704 -0
  56. python_codex-0.3.0.dist-info/RECORD +90 -0
  57. responses_server/__init__.py +1 -5
  58. responses_server/__main__.py +0 -1
  59. responses_server/app.py +36 -31
  60. responses_server/config.py +23 -23
  61. responses_server/messages_api.py +51 -53
  62. responses_server/payload_processors.py +25 -20
  63. responses_server/server.py +11 -11
  64. responses_server/session_store.py +14 -11
  65. responses_server/stream_router.py +101 -98
  66. responses_server/tools/custom_adapter.py +17 -16
  67. responses_server/tools/web_search.py +39 -36
  68. responses_server/trajectory_dump.py +36 -14
  69. workspace_server/__main__.py +0 -1
  70. workspace_server/app.py +461 -375
  71. workspace_server/workspace.html +852 -228
  72. workspace_server/workspaces.html +94 -95
  73. workspace_server/workspaces.py +137 -79
  74. pycodex/collaboration.py +0 -20
  75. pycodex/interactive_session.py +0 -415
  76. pycodex/prompts/collaboration_default.md +0 -11
  77. pycodex/prompts/collaboration_plan.md +0 -128
  78. pycodex/utils/toolcall_visualize.py +0 -713
  79. pycodex/utils/visualize.py +0 -560
  80. python_codex-0.2.7.dist-info/METADATA +0 -455
  81. python_codex-0.2.7.dist-info/RECORD +0 -93
  82. {python_codex-0.2.7.dist-info → python_codex-0.3.0.dist-info}/WHEEL +0 -0
  83. {python_codex-0.2.7.dist-info → python_codex-0.3.0.dist-info}/entry_points.txt +0 -0
  84. {python_codex-0.2.7.dist-info → python_codex-0.3.0.dist-info}/licenses/LICENSE +0 -0
pycodex/feishu_card.py CHANGED
@@ -1,11 +1,22 @@
1
1
  import json
2
2
  import os
3
- from pathlib import Path
4
3
  import time
5
4
  import typing
5
+ from pathlib import Path
6
6
 
7
7
  import requests
8
8
 
9
+ from .events import (
10
+ DEFAULT_MAIN_PROMPT,
11
+ Event,
12
+ EventDisplay,
13
+ InputQueuedEvent,
14
+ SessionStateEvent,
15
+ TurnCompletedEvent,
16
+ TurnStartedEvent,
17
+ )
18
+ from .utils.event_helpers import completed_history
19
+
9
20
  FEISHU_API_BASE = "https://open.feishu.cn/open-apis"
10
21
  FEISHU_DOMAIN = "https://open.feishu.cn"
11
22
  FEISHU_REFRESH_TOKEN_FILE = "~/.codex/.feishu_refresh_token"
@@ -13,7 +24,6 @@ CARD_OUTPUT_LIMIT = 6500
13
24
  CARD_OUTPUT_MODE_MARKDOWN = "markdown"
14
25
  CARD_OUTPUT_MODE_CODE = "code"
15
26
  CARD_CONTENT_ERROR_MARKER = "Failed to create card content"
16
- LAST_TURN_PREFIX = "(*last turn)\n"
17
27
 
18
28
 
19
29
  class PycodexCard:
@@ -25,7 +35,6 @@ class PycodexCard:
25
35
  domain: str = FEISHU_DOMAIN,
26
36
  verification_token: "typing.Union[str, None]" = None,
27
37
  encrypt_key: "typing.Union[str, None]" = None,
28
- refresh_token: "typing.Union[str, None]" = None,
29
38
  session: "typing.Union[requests.Session, None]" = None,
30
39
  ) -> None:
31
40
  self.app_id = app_id
@@ -34,22 +43,23 @@ class PycodexCard:
34
43
  self.domain = domain
35
44
  self.verification_token = verification_token
36
45
  self.encrypt_key = encrypt_key
37
- self.refresh_token = refresh_token
38
46
  self.session = session or requests.Session()
39
47
  self.message_id = None
40
48
  self.callback_token = None
41
49
  self.session_key = None
42
- self.status = "Idle"
43
- self.status_detail = "Ready."
44
- self.last_sender = "cli"
45
- self.last_prompt = ""
50
+ self.status = None
51
+ self.prompt_text = DEFAULT_MAIN_PROMPT
46
52
  self.model_name = "pycodex"
47
53
  self.output_text = ""
48
- self.working_output_text = ""
49
- self._working_delta_active = False
50
- self.error = ""
51
- self.running = False
54
+ self.activity_text = ""
55
+ self.user_prompt = ""
56
+ self.turn_history = []
57
+ self.history_index = None
58
+ self._active_turn_id = None
59
+ self._event_output = ""
60
+ self.display = EventDisplay(self._log, self._set_status, self._set_prompt)
52
61
  self.detached = False
62
+ self.accepts_input = True
53
63
  self._user_access_token = None
54
64
  self._user_token_expires_at = 0.0
55
65
  self._tenant_token = None
@@ -68,7 +78,6 @@ class PycodexCard:
68
78
  "LARK_VERIFICATION_TOKEN",
69
79
  ),
70
80
  encrypt_key=_env("FEISHU_ENCRYPT_KEY", "LARK_ENCRYPT_KEY"),
71
- refresh_token=_read_refresh_token() or os.environ.get("FEISHU_REFRESH_TOKEN"),
72
81
  )
73
82
 
74
83
  def configured(self) -> bool:
@@ -146,145 +155,92 @@ class PycodexCard:
146
155
  use_user_token=False,
147
156
  )
148
157
 
149
- def set_queued(self, prompt: str, sender: str = "cli") -> None:
150
- self.status = "Queued"
151
- self.status_detail = "Waiting for pycodex."
152
- self.last_sender = sender or "cli"
153
- self.last_prompt = prompt
154
- self._mark_last_turn_output()
155
- self._reset_working_output()
156
- self.error = ""
157
- self.running = True
158
-
159
- def set_snapshot(self, prompt: str, output: str) -> None:
160
- self.last_prompt = prompt or self.last_prompt
161
- self.output_text = output or self.output_text
162
-
163
158
  def detach(self) -> None:
164
- self.status = "Detached"
165
- self.status_detail = ""
166
- self.error = ""
167
- self.running = False
168
159
  self.detached = True
169
160
 
170
- def apply_event(self, event) -> bool:
171
- kind = getattr(event, "kind", None)
172
- if kind is None and isinstance(event, dict):
173
- kind = event.get("kind", "")
174
- kind = str(kind or "")
175
- payload = getattr(event, "payload", None)
176
- if payload is None and isinstance(event, dict):
177
- payload = event
178
- if not isinstance(payload, dict):
179
- payload = {}
180
- if kind == "turn_started":
181
- self.status = "Running"
182
- self.status_detail = "Model request started."
183
- was_running = self.running
184
- self.running = True
185
- self._reset_working_output()
186
- prompt = payload.get("user_text") or "\n".join(
187
- str(item) for item in payload.get("user_texts", []) or []
188
- )
189
- if prompt:
190
- prompt_text = str(prompt)
191
- if not (was_running and prompt_text == self.last_prompt):
192
- self.last_sender = "cli"
193
- self.last_prompt = prompt_text
161
+ def apply_event(self, event: "Event") -> None:
162
+ self._event_output = ""
163
+ if isinstance(event, SessionStateEvent):
164
+ state = event.state
165
+ self.model_name = state["model"]
166
+ self.accepts_input = state["accepts_input"]
167
+ self.display.closed = state["closed"]
168
+ if event.reason in {"attach", "history"}:
169
+ self.output_text = ""
170
+ self.activity_text = ""
171
+ self.turn_history = list(completed_history(state))
172
+ self.history_index = None
173
+ active = state["active_turn"]
174
+ self._active_turn_id = active["turn_id"] if active is not None else None
175
+ self.user_prompt = (
176
+ "\n".join(active["user_texts"])
177
+ if active is not None
178
+ else self.turn_history[-1][0] if self.turn_history else ""
179
+ )
180
+ self.display.stream_buffer = ""
181
+ self.prompt_text = DEFAULT_MAIN_PROMPT
182
+ if state["busy"]:
183
+ self._set_status("working")
184
+ else:
185
+ self.display.set_idle_status(state["background_work_count"])
186
+ for _prompt, response in self.turn_history[-1:]:
187
+ self.output_text = response
188
+ if state["busy"]:
189
+ self._mark_last_turn_output()
190
+ elif isinstance(event, (InputQueuedEvent, TurnStartedEvent)):
194
191
  self._mark_last_turn_output()
195
- self.error = ""
196
- return False
197
- if kind == "assistant_delta":
198
- self.status = "Responding"
199
- self.status_detail = "Receiving assistant output."
200
- self._append_working_delta(str(payload.get("delta") or ""))
201
- return False
202
- if kind == "tool_started":
203
- self.status = "Tool"
204
- self.status_detail = str(payload.get("tool_name") or "tool")
205
- self._finish_working_delta_segment()
206
- return False
207
- if kind == "tool_completed":
208
- self.status_detail = str(
209
- payload.get("summary") or payload.get("tool_name") or "tool completed"
210
- )
211
- self._finish_working_delta_segment()
212
- return False
213
- if kind == "stream_error":
214
- self.status = "Retrying"
215
- self.status_detail = str(
216
- payload.get("summary") or payload.get("message") or ""
217
- )
218
- return False
219
- if kind == "turn_completed":
220
- final_text = str(payload.get("output_text") or "")
221
- if final_text:
222
- self.output_text = final_text
223
- self.status = "Idle"
224
- self.status_detail = "Turn completed."
225
- self.running = False
226
- self._reset_working_output()
227
- return True
228
- if kind in {"turn_failed", "submission_failed"}:
229
- self.status = "Error"
230
- self.error = str(payload.get("error") or kind)
231
- self.status_detail = self.error
232
- self.running = False
233
- self._finish_working_delta_segment()
234
- return True
235
- if kind in {"turn_interrupted", "submission_cancelled"}:
236
- self.status = "Idle"
237
- self.status_detail = kind.replace("_", " ")
238
- self.running = False
239
- self._reset_working_output()
240
- return True
241
- return False
192
+ if isinstance(event, TurnStartedEvent):
193
+ prompt = event.visualize()
194
+ if event.turn_id == self._active_turn_id and self.user_prompt:
195
+ self.user_prompt += "\n" + prompt
196
+ else:
197
+ self.user_prompt = prompt
198
+ self._active_turn_id = event.turn_id
199
+ event.render(self.display)
200
+ if isinstance(event, TurnCompletedEvent):
201
+ if event.output_text:
202
+ self.output_text = event.output_text
203
+ self.turn_history.append((self.user_prompt, event.output_text))
204
+ self.activity_text = ""
205
+ self._active_turn_id = None
206
+ elif isinstance(event, TurnStartedEvent):
207
+ self.activity_text = ""
208
+ elif self._event_output:
209
+ self.activity_text = self._event_output
242
210
 
243
211
  def _mark_last_turn_output(self) -> None:
244
- if self.output_text and not self.output_text.startswith(LAST_TURN_PREFIX):
245
- self.output_text = LAST_TURN_PREFIX + self.output_text
212
+ prefix = "(*last turn)\n"
213
+ if self.output_text and not self.output_text.startswith(prefix):
214
+ self.output_text = prefix + self.output_text
246
215
 
247
- def _reset_working_output(self) -> None:
248
- self.working_output_text = ""
249
- self._working_delta_active = False
216
+ def _log(self, text: str) -> None:
217
+ self._event_output = (
218
+ self._event_output + ("\n" if self._event_output else "") + text
219
+ )[-CARD_OUTPUT_LIMIT:]
250
220
 
251
- def _finish_working_delta_segment(self) -> None:
252
- self._working_delta_active = False
221
+ def _set_status(self, text: "typing.Union[str, None]") -> None:
222
+ self.status = text
253
223
 
254
- def _append_working_delta(self, delta: str) -> None:
255
- if not delta:
256
- return
257
- if self._working_delta_active:
258
- self.working_output_text += delta
259
- else:
260
- self.working_output_text = delta
261
- self._working_delta_active = True
224
+ def _set_prompt(self, text: str) -> None:
225
+ if text != self.prompt_text:
226
+ self.activity_text = ""
227
+ self.prompt_text = text
262
228
 
263
229
  def render(
264
230
  self, output_mode: str = CARD_OUTPUT_MODE_MARKDOWN
265
231
  ) -> "typing.Dict[str, object]":
266
- idle = str(self.status or "").lower() == "idle"
267
- status_line = _escape_markdown(self.status)
268
- if self.status_detail:
269
- status_line += " - " + _escape_markdown(self.status_detail)
270
- if self.error:
271
- status_line += " - " + _escape_markdown(self.error)
272
- input_disabled = self.running or self.detached
273
- sender = _escape_markdown(self.last_sender or "cli")
274
- prompt = _truncate(self.last_prompt, 1200) or "-"
275
- output = _truncate(self.output_text, CARD_OUTPUT_LIMIT) or (
276
- "Waiting for output..." if self.running else "Ready."
277
- )
278
- output_content = _render_output_content(output, output_mode)
279
- working_output = _truncate(self.working_output_text, CARD_OUTPUT_LIMIT)
280
- color, title = _status_template(self.status)
232
+ status = "Closed" if self.display.closed else self.status
233
+ input_disabled = not self.accepts_input or self.detached or self.display.closed
234
+ output = _truncate(self.output_text, CARD_OUTPUT_LIMIT) or "Ready."
235
+ working_output = _truncate(self.display.stream_buffer, CARD_OUTPUT_LIMIT)
236
+ color = _status_color("Detached" if self.detached else status or "Idle")
281
237
  body_elements = [
282
- {
283
- "tag": "markdown",
284
- "element_id": "prompt_md",
285
- "content": f"> {sender}: **{_escape_code_block(prompt)}**",
286
- },
287
- _output_box("answer_box", "answer_md", output_content, "grey-50"),
238
+ _output_box(
239
+ "answer_box",
240
+ "answer_md",
241
+ _render_output_content(output, output_mode),
242
+ "grey-50",
243
+ ),
288
244
  ]
289
245
  if working_output:
290
246
  body_elements.append(
@@ -295,11 +251,49 @@ class PycodexCard:
295
251
  "green-50",
296
252
  )
297
253
  )
254
+ if self.activity_text:
255
+ body_elements.append(
256
+ {
257
+ "tag": "markdown",
258
+ "element_id": "activity_md",
259
+ "content": _render_output_content(self.activity_text, output_mode),
260
+ }
261
+ )
262
+ if self.user_prompt:
263
+ body_elements.insert(
264
+ 0,
265
+ {
266
+ "tag": "markdown",
267
+ "element_id": "user_prompt_md",
268
+ "content": _render_output_content(
269
+ "user> " + _truncate(self.user_prompt, CARD_OUTPUT_LIMIT),
270
+ output_mode,
271
+ ),
272
+ },
273
+ )
274
+ history = self.earlier_turns()
275
+ if history:
276
+ index = (
277
+ len(history) - 1 if self.history_index is None else self.history_index
278
+ )
279
+ body_elements.insert(
280
+ 0,
281
+ _history_panel(
282
+ history,
283
+ index,
284
+ self.history_index is not None,
285
+ output_mode,
286
+ not self.detached,
287
+ ),
288
+ )
298
289
  card = {
299
290
  "schema": "2.0",
300
291
  "config": {"update_multi": True},
301
292
  "header": {
302
- "title": {"tag": "plain_text", "content": title},
293
+ "title": {
294
+ "tag": "plain_text",
295
+ "content": self.display.title or "pycodex",
296
+ },
303
297
  "template": color,
304
298
  },
305
299
  "body": {
@@ -317,7 +311,7 @@ class PycodexCard:
317
311
  "disabled": input_disabled,
318
312
  "placeholder": {
319
313
  "tag": "plain_text",
320
- "content": f"Ask {self.model_name}..." if idle else status_line,
314
+ "content": self.prompt_text + (status or self.model_name),
321
315
  },
322
316
  "behaviors": [{"type": "callback", "value": {"action": "send"}}],
323
317
  "value": {"action": "send"},
@@ -325,6 +319,16 @@ class PycodexCard:
325
319
  )
326
320
  return card
327
321
 
322
+ def earlier_turns(self):
323
+ if self._active_turn_id is not None:
324
+ return self.turn_history
325
+ return self.turn_history[:-1]
326
+
327
+ def show_history(self, index):
328
+ if type(index) is not int or not 0 <= index < len(self.earlier_turns()):
329
+ raise ValueError("This history turn is no longer available.")
330
+ self.history_index = index
331
+
328
332
  def parse_action(self, sdk_event) -> "typing.Dict[str, object]":
329
333
  event_data = getattr(sdk_event, "event", None)
330
334
  action_payload = getattr(event_data, "action", None)
@@ -360,6 +364,7 @@ class PycodexCard:
360
364
  self.session_key = _default_session_key(tenant_key, open_id, message_id)
361
365
  return {
362
366
  "action": action,
367
+ "history_index": value.get("history_index"),
363
368
  "prompt": prompt,
364
369
  "sender": self.resolve_operator_name(operator),
365
370
  }
@@ -470,10 +475,9 @@ class PycodexCard:
470
475
  )
471
476
 
472
477
  def user_access_token(self) -> "typing.Union[str, None]":
473
- refresh_token = _read_refresh_token() or self.refresh_token
478
+ refresh_token = _read_refresh_token()
474
479
  if not refresh_token:
475
480
  return None
476
- self.refresh_token = refresh_token
477
481
  now = time.time()
478
482
  if self._user_access_token and now < self._user_token_expires_at:
479
483
  return self._user_access_token
@@ -493,14 +497,12 @@ class PycodexCard:
493
497
  token = payload.get("access_token")
494
498
  if not token:
495
499
  raise RuntimeError("user access_token missing from Feishu response")
500
+ refresh_token = payload.get("refresh_token")
501
+ if refresh_token:
502
+ _write_refresh_token(str(refresh_token))
496
503
  self._user_access_token = str(token)
497
504
  expires_in = payload.get("expires_in") or 0
498
505
  self._user_token_expires_at = time.time() + max(60, int(expires_in) - 120)
499
- refresh_token = payload.get("refresh_token")
500
- if refresh_token:
501
- self.refresh_token = str(refresh_token)
502
- os.environ["FEISHU_REFRESH_TOKEN"] = self.refresh_token
503
- _write_refresh_token(self.refresh_token)
504
506
  return self._user_access_token
505
507
 
506
508
  def tenant_access_token(self) -> str:
@@ -601,15 +603,13 @@ def _display_user_name(user: "typing.Any") -> "typing.Union[str, None]":
601
603
  return None
602
604
 
603
605
 
604
- def _status_template(status: str) -> "typing.Tuple[str, str]":
606
+ def _status_color(status: str) -> str:
605
607
  normalized = status.lower()
606
- if normalized in {"error", "failed"}:
607
- return "red", "Session Connected"
608
- if normalized in {"running", "responding", "tool", "queued", "retrying"}:
609
- return "blue", "Session Connected"
610
- if normalized in {"detached"}:
611
- return "grey", "Session Detached"
612
- return "green", "Session Connected"
608
+ if normalized in {"detached", "closed"}:
609
+ return "grey"
610
+ if normalized in {"idle", "idle: sleeping"}:
611
+ return "green"
612
+ return "blue"
613
613
 
614
614
 
615
615
  def _truncate(text: str, limit: int) -> str:
@@ -629,6 +629,65 @@ def _render_output_content(output: str, output_mode: str) -> str:
629
629
  return output
630
630
 
631
631
 
632
+ def _history_panel(history, index, expanded, output_mode, interactive):
633
+ prompt, response = history[index]
634
+ content = _truncate(
635
+ "user> {0}\n\nassistant> {1}".format(prompt, response), CARD_OUTPUT_LIMIT
636
+ )
637
+ columns = []
638
+ for label, target, disabled in (
639
+ ("Previous", index - 1, index == 0),
640
+ ("Next", index + 1, index == len(history) - 1),
641
+ ):
642
+ columns.append(
643
+ {
644
+ "tag": "column",
645
+ "width": "auto",
646
+ "elements": [
647
+ {
648
+ "tag": "button",
649
+ "text": {"tag": "plain_text", "content": label},
650
+ "type": "default",
651
+ "disabled": disabled or not interactive,
652
+ "behaviors": [
653
+ {
654
+ "type": "callback",
655
+ "value": {
656
+ "action": "history",
657
+ "history_index": target,
658
+ },
659
+ }
660
+ ],
661
+ }
662
+ ],
663
+ }
664
+ )
665
+ return {
666
+ "tag": "collapsible_panel",
667
+ "element_id": "history_panel",
668
+ "expanded": expanded,
669
+ "header": {
670
+ "icon": {
671
+ "tag": "standard_icon",
672
+ "token": "down-small-ccm_outlined",
673
+ "size": "16px 16px",
674
+ },
675
+ "title": {
676
+ "tag": "plain_text",
677
+ "content": "History · {0} / {1}".format(index + 1, len(history)),
678
+ },
679
+ },
680
+ "elements": [
681
+ {
682
+ "tag": "markdown",
683
+ "element_id": "history_md",
684
+ "content": _render_output_content(content, output_mode),
685
+ },
686
+ {"tag": "column_set", "columns": columns},
687
+ ],
688
+ }
689
+
690
+
632
691
  def _output_box(
633
692
  box_id: str,
634
693
  markdown_id: str,
@@ -664,10 +723,6 @@ def _is_card_content_error(exc: "BaseException") -> bool:
664
723
  return CARD_CONTENT_ERROR_MARKER in str(exc)
665
724
 
666
725
 
667
- def _escape_markdown(text: str) -> str:
668
- return str(text or "").replace("`", "'")
669
-
670
-
671
726
  def _dig(value: "typing.Any", *path: str) -> "typing.Any":
672
727
  current = value
673
728
  for key in path:
@@ -700,11 +755,10 @@ def _read_refresh_token() -> "typing.Union[str, None]":
700
755
  def _write_refresh_token(value: str) -> None:
701
756
  path = _refresh_token_path()
702
757
  path.parent.mkdir(parents=True, exist_ok=True)
703
- path.write_text("{0}\n".format(value), encoding="utf-8")
704
- try:
758
+ descriptor = os.open(str(path), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
759
+ with os.fdopen(descriptor, "w", encoding="utf-8") as stream:
705
760
  path.chmod(0o600)
706
- except OSError:
707
- pass
761
+ stream.write("{0}\n".format(value))
708
762
 
709
763
 
710
764
  def _api_base_to_domain(api_base: str) -> str: