crabcode 0.1.6__py3-none-win_amd64.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.
- crabcode-0.1.6.dist-info/METADATA +63 -0
- crabcode-0.1.6.dist-info/RECORD +205 -0
- crabcode-0.1.6.dist-info/WHEEL +4 -0
- crabcode-0.1.6.dist-info/entry_points.txt +2 -0
- crabcode_cli/__init__.py +7 -0
- crabcode_cli/__main__.py +4 -0
- crabcode_cli/_native/crabcode-computer-use-host.exe +0 -0
- crabcode_cli/_native/runtime.json +6 -0
- crabcode_cli/app.py +776 -0
- crabcode_cli/banner.py +39 -0
- crabcode_cli/computer_use.py +286 -0
- crabcode_cli/model_manager.py +649 -0
- crabcode_cli/model_picker.py +168 -0
- crabcode_cli/pipe.py +148 -0
- crabcode_cli/render/__init__.py +0 -0
- crabcode_cli/render/messages.py +19 -0
- crabcode_cli/repl.py +4841 -0
- crabcode_cli/session_picker.py +239 -0
- crabcode_core/__init__.py +7 -0
- crabcode_core/_version.py +3 -0
- crabcode_core/_windows_process.py +68 -0
- crabcode_core/agent_manager.py +1731 -0
- crabcode_core/api/__init__.py +20 -0
- crabcode_core/api/anthropic_adapter.py +648 -0
- crabcode_core/api/azure_adapter.py +69 -0
- crabcode_core/api/base.py +233 -0
- crabcode_core/api/codex_adapter.py +1327 -0
- crabcode_core/api/error_logging.py +132 -0
- crabcode_core/api/gemini_adapter.py +26 -0
- crabcode_core/api/model_info.py +257 -0
- crabcode_core/api/network.py +62 -0
- crabcode_core/api/ollama_adapter.py +27 -0
- crabcode_core/api/openai_adapter.py +323 -0
- crabcode_core/api/registry.py +58 -0
- crabcode_core/command_line.py +18 -0
- crabcode_core/compact/__init__.py +17 -0
- crabcode_core/compact/compact.py +514 -0
- crabcode_core/compact/context_tokens.py +165 -0
- crabcode_core/computer_use_mode.py +38 -0
- crabcode_core/computer_use_observation.py +144 -0
- crabcode_core/config/__init__.py +3 -0
- crabcode_core/config/claudemd.py +61 -0
- crabcode_core/config/manager.py +237 -0
- crabcode_core/config/model_catalog.py +256 -0
- crabcode_core/events.py +4896 -0
- crabcode_core/file_lock.py +81 -0
- crabcode_core/filesystem.py +22 -0
- crabcode_core/goal.py +149 -0
- crabcode_core/hooks/__init__.py +5 -0
- crabcode_core/hooks/manager.py +290 -0
- crabcode_core/image_io.py +111 -0
- crabcode_core/io_worker.py +211 -0
- crabcode_core/logging_utils.py +168 -0
- crabcode_core/lsp/__init__.py +6 -0
- crabcode_core/lsp/client.py +786 -0
- crabcode_core/lsp/diagnostics.py +150 -0
- crabcode_core/lsp/manager.py +583 -0
- crabcode_core/mcp/__init__.py +4 -0
- crabcode_core/mcp/client.py +169 -0
- crabcode_core/mcp/config.py +57 -0
- crabcode_core/path_validation.py +34 -0
- crabcode_core/paths.py +24 -0
- crabcode_core/peer/__init__.py +10 -0
- crabcode_core/peer/runtime.py +518 -0
- crabcode_core/permissions/__init__.py +7 -0
- crabcode_core/permissions/ai_reviewer.py +184 -0
- crabcode_core/permissions/manager.py +187 -0
- crabcode_core/plan/__init__.py +5 -0
- crabcode_core/plan/executor.py +270 -0
- crabcode_core/plan/types.py +169 -0
- crabcode_core/prompts/__init__.py +0 -0
- crabcode_core/prompts/blocks.py +10 -0
- crabcode_core/prompts/context.py +229 -0
- crabcode_core/prompts/library.py +92 -0
- crabcode_core/prompts/profile.py +79 -0
- crabcode_core/prompts/system.py +356 -0
- crabcode_core/prompts/templates.py +104 -0
- crabcode_core/query/__init__.py +0 -0
- crabcode_core/query/computer_use_history.py +83 -0
- crabcode_core/query/loop.py +2446 -0
- crabcode_core/query/retry.py +116 -0
- crabcode_core/schedule/__init__.py +13 -0
- crabcode_core/schedule/manager.py +631 -0
- crabcode_core/schedule/models.py +204 -0
- crabcode_core/schedule/store.py +558 -0
- crabcode_core/schedule/timing.py +287 -0
- crabcode_core/session/__init__.py +12 -0
- crabcode_core/session/export.py +141 -0
- crabcode_core/session/meta_db.py +684 -0
- crabcode_core/session/storage.py +2032 -0
- crabcode_core/session/title_gen.py +52 -0
- crabcode_core/skills/__init__.py +6 -0
- crabcode_core/skills/context.py +23 -0
- crabcode_core/skills/loader.py +214 -0
- crabcode_core/skills/matcher.py +203 -0
- crabcode_core/snapshot/__init__.py +21 -0
- crabcode_core/snapshot/snapshot.py +436 -0
- crabcode_core/snapshot/tracker.py +359 -0
- crabcode_core/subprocess_utils.py +260 -0
- crabcode_core/team/__init__.py +27 -0
- crabcode_core/team/bridge.py +66 -0
- crabcode_core/team/inbox.py +237 -0
- crabcode_core/team/manager.py +1660 -0
- crabcode_core/team/message_bus.py +821 -0
- crabcode_core/team/models.py +191 -0
- crabcode_core/team/recovery.py +192 -0
- crabcode_core/text_io.py +52 -0
- crabcode_core/tools/__init__.py +112 -0
- crabcode_core/tools/_input_helpers.py +39 -0
- crabcode_core/tools/_input_schema.py +130 -0
- crabcode_core/tools/agent.py +413 -0
- crabcode_core/tools/apply_patch.py +587 -0
- crabcode_core/tools/ask_user.py +122 -0
- crabcode_core/tools/bash.py +154 -0
- crabcode_core/tools/browser.py +678 -0
- crabcode_core/tools/checklist.py +241 -0
- crabcode_core/tools/checkpoint.py +78 -0
- crabcode_core/tools/computer_use.py +599 -0
- crabcode_core/tools/diff_utils.py +123 -0
- crabcode_core/tools/file_edit.py +240 -0
- crabcode_core/tools/file_read.py +187 -0
- crabcode_core/tools/file_write.py +154 -0
- crabcode_core/tools/glob.py +123 -0
- crabcode_core/tools/goal.py +134 -0
- crabcode_core/tools/grep.py +247 -0
- crabcode_core/tools/image.py +173 -0
- crabcode_core/tools/image_generate.py +115 -0
- crabcode_core/tools/lint.py +388 -0
- crabcode_core/tools/loading.py +187 -0
- crabcode_core/tools/memory.py +456 -0
- crabcode_core/tools/monitor.py +677 -0
- crabcode_core/tools/peer.py +103 -0
- crabcode_core/tools/revert.py +90 -0
- crabcode_core/tools/schedule.py +683 -0
- crabcode_core/tools/skill.py +108 -0
- crabcode_core/tools/switch_mode.py +213 -0
- crabcode_core/tools/team.py +329 -0
- crabcode_core/tools/tool_search.py +64 -0
- crabcode_core/tools/web_search.py +444 -0
- crabcode_core/turn_details.py +64 -0
- crabcode_core/types/__init__.py +95 -0
- crabcode_core/types/config.py +434 -0
- crabcode_core/types/event.py +334 -0
- crabcode_core/types/message.py +263 -0
- crabcode_core/types/tool.py +208 -0
- crabcode_core/usage.py +289 -0
- crabcode_core/utf8_sanitize.py +29 -0
- crabcode_debugger/__init__.py +5 -0
- crabcode_debugger/adapters.py +332 -0
- crabcode_debugger/dap.py +304 -0
- crabcode_debugger/memory.py +1806 -0
- crabcode_debugger/process.py +707 -0
- crabcode_debugger/runtime.py +58 -0
- crabcode_debugger/sessions.py +387 -0
- crabcode_debugger/tool.py +731 -0
- crabcode_gateway/__init__.py +18 -0
- crabcode_gateway/acp/__init__.py +13 -0
- crabcode_gateway/acp/agent.py +1581 -0
- crabcode_gateway/acp/session.py +114 -0
- crabcode_gateway/acp/transport.py +39 -0
- crabcode_gateway/acp/types.py +90 -0
- crabcode_gateway/adapter.py +30 -0
- crabcode_gateway/auth.py +139 -0
- crabcode_gateway/computer_use.py +364 -0
- crabcode_gateway/document_engine.py +637 -0
- crabcode_gateway/document_worker.py +301 -0
- crabcode_gateway/event_bus.py +655 -0
- crabcode_gateway/grpc/__init__.py +0 -0
- crabcode_gateway/grpc/crabcode_pb2.py +70 -0
- crabcode_gateway/grpc/crabcode_pb2_grpc.py +474 -0
- crabcode_gateway/grpc/proto/crabcode.proto +160 -0
- crabcode_gateway/grpc/server.py +765 -0
- crabcode_gateway/middleware.py +344 -0
- crabcode_gateway/protocol.py +17 -0
- crabcode_gateway/routes/__init__.py +0 -0
- crabcode_gateway/routes/agent.py +392 -0
- crabcode_gateway/routes/auth.py +150 -0
- crabcode_gateway/routes/computer_use.py +101 -0
- crabcode_gateway/routes/config.py +1698 -0
- crabcode_gateway/routes/document.py +1810 -0
- crabcode_gateway/routes/event.py +3801 -0
- crabcode_gateway/routes/git_review.py +232 -0
- crabcode_gateway/routes/health.py +14 -0
- crabcode_gateway/routes/peer.py +78 -0
- crabcode_gateway/routes/permission.py +121 -0
- crabcode_gateway/routes/schedule.py +300 -0
- crabcode_gateway/routes/session.py +1496 -0
- crabcode_gateway/routes/snapshot.py +211 -0
- crabcode_gateway/routes/tasks.py +318 -0
- crabcode_gateway/routes/team.py +392 -0
- crabcode_gateway/routes/usage.py +49 -0
- crabcode_gateway/routes/workspace.py +299 -0
- crabcode_gateway/safe_file.py +63 -0
- crabcode_gateway/schemas.py +1820 -0
- crabcode_gateway/server.py +514 -0
- crabcode_gateway/session_registry.py +36 -0
- crabcode_gateway/task_registry.py +708 -0
- crabcode_gateway/turn_details.py +60 -0
- crabcode_search/__init__.py +11 -0
- crabcode_search/background.py +368 -0
- crabcode_search/chunker.py +517 -0
- crabcode_search/embedder.py +497 -0
- crabcode_search/indexer.py +215 -0
- crabcode_search/store.py +222 -0
- crabcode_search/tool.py +268 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"""Safe local mutations for the named model and group catalog."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import codecs
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import stat
|
|
9
|
+
import tempfile
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, Literal
|
|
13
|
+
|
|
14
|
+
from crabcode_core.config.manager import ConfigManager, SETTING_SOURCES
|
|
15
|
+
from crabcode_core.filesystem import replace_with_retry
|
|
16
|
+
from crabcode_core.text_io import normalize_newlines, read_utf8_text
|
|
17
|
+
from crabcode_core.types.config import ApiConfig
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
CatalogKind = Literal["model", "group"]
|
|
21
|
+
WritableSettingsSource = Literal["userSettings", "projectSettings", "localSettings"]
|
|
22
|
+
|
|
23
|
+
WRITABLE_SETTINGS_SOURCES: tuple[WritableSettingsSource, ...] = (
|
|
24
|
+
"projectSettings",
|
|
25
|
+
"localSettings",
|
|
26
|
+
"userSettings",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
SETTINGS_SOURCE_LABELS: dict[str, str] = {
|
|
30
|
+
"userSettings": "User · global",
|
|
31
|
+
"projectSettings": "Project · shared",
|
|
32
|
+
"localSettings": "Project · local",
|
|
33
|
+
"flagSettings": "CLI flags · read-only",
|
|
34
|
+
"policySettings": "Managed policy · read-only",
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ModelCatalogError(ValueError):
|
|
39
|
+
"""A catalog mutation could not be completed safely."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class CatalogDefinition:
|
|
44
|
+
kind: CatalogKind
|
|
45
|
+
name: str
|
|
46
|
+
source: str
|
|
47
|
+
path: Path
|
|
48
|
+
config: dict[str, Any]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True)
|
|
52
|
+
class CatalogMutationResult:
|
|
53
|
+
action: Literal["add", "delete"]
|
|
54
|
+
kind: CatalogKind
|
|
55
|
+
name: str
|
|
56
|
+
source: WritableSettingsSource
|
|
57
|
+
path: Path
|
|
58
|
+
cleared_default: bool = False
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def validate_catalog_name(name: str) -> str:
|
|
62
|
+
"""Validate the simple names accepted by the Gateway settings editor."""
|
|
63
|
+
value = name.strip()
|
|
64
|
+
if not value:
|
|
65
|
+
raise ModelCatalogError("Name is required.")
|
|
66
|
+
if value in {".", ".."} or any(char in value for char in "/\\"):
|
|
67
|
+
raise ModelCatalogError("Name must be a simple configuration name without / or \\.")
|
|
68
|
+
if len(value) > 120:
|
|
69
|
+
raise ModelCatalogError("Name must be 120 characters or fewer.")
|
|
70
|
+
return value
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def settings_source_path(cwd: str, source: WritableSettingsSource) -> Path:
|
|
74
|
+
manager = ConfigManager(cwd=cwd)
|
|
75
|
+
path_value = manager.settings_file_paths.get(source)
|
|
76
|
+
if not path_value:
|
|
77
|
+
raise ModelCatalogError(f"Settings source is unavailable: {source}")
|
|
78
|
+
path = Path(path_value)
|
|
79
|
+
if source in {"projectSettings", "localSettings"}:
|
|
80
|
+
expected_parent = (Path(cwd).resolve() / ".crabcode").resolve()
|
|
81
|
+
try:
|
|
82
|
+
path.parent.resolve().relative_to(expected_parent)
|
|
83
|
+
except ValueError as exc:
|
|
84
|
+
raise ModelCatalogError("Project settings path is outside the workspace.") from exc
|
|
85
|
+
return path
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def list_catalog_definitions(
|
|
89
|
+
cwd: str,
|
|
90
|
+
kind: CatalogKind | None = None,
|
|
91
|
+
*,
|
|
92
|
+
writable_only: bool = False,
|
|
93
|
+
) -> list[CatalogDefinition]:
|
|
94
|
+
"""Return raw definitions, retaining the layer each definition belongs to."""
|
|
95
|
+
manager = ConfigManager(cwd=cwd)
|
|
96
|
+
sources = (
|
|
97
|
+
list(WRITABLE_SETTINGS_SOURCES)
|
|
98
|
+
if writable_only
|
|
99
|
+
else list(reversed(SETTING_SOURCES))
|
|
100
|
+
)
|
|
101
|
+
definitions: list[CatalogDefinition] = []
|
|
102
|
+
kinds: tuple[CatalogKind, ...] = (kind,) if kind else ("model", "group")
|
|
103
|
+
for source in sources:
|
|
104
|
+
raw = manager.get_settings_for_source(source)
|
|
105
|
+
path_value = manager.settings_file_paths.get(source)
|
|
106
|
+
if raw is None or not path_value:
|
|
107
|
+
continue
|
|
108
|
+
for entry_kind in kinds:
|
|
109
|
+
values = raw.get("models" if entry_kind == "model" else "groups")
|
|
110
|
+
if not isinstance(values, dict):
|
|
111
|
+
continue
|
|
112
|
+
for name, config in values.items():
|
|
113
|
+
if not isinstance(config, dict):
|
|
114
|
+
continue
|
|
115
|
+
definitions.append(
|
|
116
|
+
CatalogDefinition(
|
|
117
|
+
kind=entry_kind,
|
|
118
|
+
name=str(name),
|
|
119
|
+
source=source,
|
|
120
|
+
path=Path(path_value),
|
|
121
|
+
config=dict(config),
|
|
122
|
+
)
|
|
123
|
+
)
|
|
124
|
+
return definitions
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def catalog_names(cwd: str, kind: CatalogKind) -> set[str]:
|
|
128
|
+
return {definition.name for definition in list_catalog_definitions(cwd, kind)}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def referencing_models(cwd: str, group_name: str) -> list[str]:
|
|
132
|
+
"""List effective named models that still point at a group."""
|
|
133
|
+
settings = ConfigManager(cwd=cwd).load()
|
|
134
|
+
return sorted(name for name, config in settings.models.items() if config.group == group_name)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def add_catalog_entry(
|
|
138
|
+
cwd: str,
|
|
139
|
+
source: WritableSettingsSource,
|
|
140
|
+
kind: CatalogKind,
|
|
141
|
+
name: str,
|
|
142
|
+
config: dict[str, Any],
|
|
143
|
+
) -> CatalogMutationResult:
|
|
144
|
+
"""Add a new named entry without overwriting any existing layer."""
|
|
145
|
+
name = validate_catalog_name(name)
|
|
146
|
+
if name in catalog_names(cwd, kind):
|
|
147
|
+
raise ModelCatalogError(
|
|
148
|
+
f'{kind.title()} "{name}" already exists in the merged configuration.'
|
|
149
|
+
)
|
|
150
|
+
if not isinstance(config, dict):
|
|
151
|
+
raise ModelCatalogError("Configuration must be a JSON object.")
|
|
152
|
+
if kind == "group" and "group" in config:
|
|
153
|
+
raise ModelCatalogError("A group configuration cannot inherit from another group.")
|
|
154
|
+
try:
|
|
155
|
+
ApiConfig.model_validate(config)
|
|
156
|
+
except Exception as exc:
|
|
157
|
+
raise ModelCatalogError(f"Invalid {kind} configuration: {exc}") from exc
|
|
158
|
+
|
|
159
|
+
path = settings_source_path(cwd, source)
|
|
160
|
+
current = _read_settings_object(path)
|
|
161
|
+
key = "models" if kind == "model" else "groups"
|
|
162
|
+
values = current.get(key)
|
|
163
|
+
if values is None:
|
|
164
|
+
values = {}
|
|
165
|
+
current[key] = values
|
|
166
|
+
if not isinstance(values, dict):
|
|
167
|
+
raise ModelCatalogError(f'"{key}" in {path} must be a JSON object.')
|
|
168
|
+
if name in values:
|
|
169
|
+
raise ModelCatalogError(f'{kind.title()} "{name}" already exists in {path}.')
|
|
170
|
+
values[name] = dict(config)
|
|
171
|
+
_atomic_write_settings(path, current)
|
|
172
|
+
return CatalogMutationResult("add", kind, name, source, path)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def delete_catalog_entry(
|
|
176
|
+
cwd: str,
|
|
177
|
+
source: WritableSettingsSource,
|
|
178
|
+
kind: CatalogKind,
|
|
179
|
+
name: str,
|
|
180
|
+
) -> CatalogMutationResult:
|
|
181
|
+
"""Delete exactly one raw definition from one writable settings layer."""
|
|
182
|
+
name = validate_catalog_name(name)
|
|
183
|
+
path = settings_source_path(cwd, source)
|
|
184
|
+
current = _read_settings_object(path)
|
|
185
|
+
key = "models" if kind == "model" else "groups"
|
|
186
|
+
values = current.get(key)
|
|
187
|
+
if not isinstance(values, dict) or name not in values:
|
|
188
|
+
raise ModelCatalogError(f'{kind.title()} "{name}" is not defined in {path}.')
|
|
189
|
+
values.pop(name)
|
|
190
|
+
if not values:
|
|
191
|
+
current.pop(key, None)
|
|
192
|
+
|
|
193
|
+
cleared_default = kind == "model" and current.get("default_model") == name
|
|
194
|
+
if cleared_default:
|
|
195
|
+
# An explicit null prevents a lower-precedence default from silently
|
|
196
|
+
# becoming active when its selected model may no longer exist.
|
|
197
|
+
current["default_model"] = None
|
|
198
|
+
|
|
199
|
+
_atomic_write_settings(path, current)
|
|
200
|
+
return CatalogMutationResult(
|
|
201
|
+
"delete", kind, name, source, path, cleared_default=cleared_default
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _read_settings_object(path: Path) -> dict[str, Any]:
|
|
206
|
+
if not path.exists():
|
|
207
|
+
return {}
|
|
208
|
+
try:
|
|
209
|
+
value = json.loads(read_utf8_text(path).text)
|
|
210
|
+
except (json.JSONDecodeError, OSError, UnicodeError) as exc:
|
|
211
|
+
raise ModelCatalogError(f"Settings file is not valid UTF-8 JSON: {path}") from exc
|
|
212
|
+
if not isinstance(value, dict):
|
|
213
|
+
raise ModelCatalogError(f"Settings file must contain a JSON object: {path}")
|
|
214
|
+
return value
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _atomic_write_settings(path: Path, value: dict[str, Any]) -> None:
|
|
218
|
+
payload = json.dumps(value, indent=2, ensure_ascii=False) + "\n"
|
|
219
|
+
newline = "\n"
|
|
220
|
+
has_bom = False
|
|
221
|
+
if path.exists():
|
|
222
|
+
try:
|
|
223
|
+
source = read_utf8_text(path)
|
|
224
|
+
except (OSError, UnicodeError) as exc:
|
|
225
|
+
raise ModelCatalogError(f"Could not read settings file: {path}") from exc
|
|
226
|
+
newline = source.newline or newline
|
|
227
|
+
has_bom = source.has_bom
|
|
228
|
+
payload = normalize_newlines(payload, newline)
|
|
229
|
+
raw_payload = payload.encode("utf-8")
|
|
230
|
+
if has_bom:
|
|
231
|
+
raw_payload = codecs.BOM_UTF8 + raw_payload
|
|
232
|
+
|
|
233
|
+
temporary: Path | None = None
|
|
234
|
+
try:
|
|
235
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
236
|
+
with tempfile.NamedTemporaryFile(
|
|
237
|
+
"wb",
|
|
238
|
+
dir=path.parent,
|
|
239
|
+
prefix=f".{path.name}.",
|
|
240
|
+
suffix=".tmp",
|
|
241
|
+
delete=False,
|
|
242
|
+
) as handle:
|
|
243
|
+
temporary = Path(handle.name)
|
|
244
|
+
handle.write(raw_payload)
|
|
245
|
+
handle.flush()
|
|
246
|
+
os.fsync(handle.fileno())
|
|
247
|
+
if path.exists():
|
|
248
|
+
temporary.chmod(stat.S_IMODE(path.stat().st_mode))
|
|
249
|
+
replace_with_retry(temporary, path)
|
|
250
|
+
except OSError as exc:
|
|
251
|
+
if temporary is not None:
|
|
252
|
+
try:
|
|
253
|
+
temporary.unlink(missing_ok=True)
|
|
254
|
+
except OSError:
|
|
255
|
+
pass
|
|
256
|
+
raise ModelCatalogError(f"Settings file is not writable: {path}") from exc
|