universal-agent-context 0.2.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.
- uacs/__init__.py +12 -0
- uacs/adapters/__init__.py +19 -0
- uacs/adapters/agent_skill_adapter.py +202 -0
- uacs/adapters/agents_md_adapter.py +330 -0
- uacs/adapters/base.py +261 -0
- uacs/adapters/clinerules_adapter.py +39 -0
- uacs/adapters/cursorrules_adapter.py +39 -0
- uacs/api.py +262 -0
- uacs/cli/__init__.py +6 -0
- uacs/cli/context.py +349 -0
- uacs/cli/main.py +195 -0
- uacs/cli/mcp.py +115 -0
- uacs/cli/memory.py +142 -0
- uacs/cli/packages.py +309 -0
- uacs/cli/skills.py +144 -0
- uacs/cli/utils.py +24 -0
- uacs/config/repositories.yaml +26 -0
- uacs/context/__init__.py +0 -0
- uacs/context/agent_context.py +406 -0
- uacs/context/shared_context.py +661 -0
- uacs/context/unified_context.py +332 -0
- uacs/mcp_server_entry.py +80 -0
- uacs/memory/__init__.py +5 -0
- uacs/memory/simple_memory.py +255 -0
- uacs/packages/__init__.py +26 -0
- uacs/packages/manager.py +413 -0
- uacs/packages/models.py +60 -0
- uacs/packages/sources.py +270 -0
- uacs/protocols/__init__.py +5 -0
- uacs/protocols/mcp/__init__.py +8 -0
- uacs/protocols/mcp/manager.py +77 -0
- uacs/protocols/mcp/skills_server.py +700 -0
- uacs/skills_validator.py +367 -0
- uacs/utils/__init__.py +5 -0
- uacs/utils/paths.py +24 -0
- uacs/visualization/README.md +132 -0
- uacs/visualization/__init__.py +36 -0
- uacs/visualization/models.py +195 -0
- uacs/visualization/static/index.html +857 -0
- uacs/visualization/storage.py +402 -0
- uacs/visualization/visualization.py +328 -0
- uacs/visualization/web_server.py +364 -0
- universal_agent_context-0.2.0.dist-info/METADATA +873 -0
- universal_agent_context-0.2.0.dist-info/RECORD +47 -0
- universal_agent_context-0.2.0.dist-info/WHEEL +4 -0
- universal_agent_context-0.2.0.dist-info/entry_points.txt +2 -0
- universal_agent_context-0.2.0.dist-info/licenses/LICENSE +21 -0
uacs/packages/manager.py
ADDED
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
"""Package manager for installing and managing UACS skills.
|
|
2
|
+
|
|
3
|
+
Provides a minimal feature set similar to GitHub CLI extensions:
|
|
4
|
+
- Install packages from GitHub, Git URLs, or local paths
|
|
5
|
+
- List installed packages
|
|
6
|
+
- Validate packages
|
|
7
|
+
- Uninstall packages
|
|
8
|
+
- Update packages
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import re
|
|
13
|
+
import shutil
|
|
14
|
+
import tempfile
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from uacs.packages.models import InstalledPackage, PackageSource
|
|
19
|
+
from uacs.skills_validator import SkillValidator, ValidationResult
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class PackageManagerError(Exception):
|
|
23
|
+
"""Base exception for package manager errors."""
|
|
24
|
+
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class PackageSourceHandler:
|
|
29
|
+
"""Handles parsing and fetching package sources."""
|
|
30
|
+
|
|
31
|
+
GITHUB_PATTERN = re.compile(r"^([a-zA-Z0-9_-]+)/([a-zA-Z0-9_.-]+)$")
|
|
32
|
+
GIT_URL_PATTERN = re.compile(r"^(https?://|git@|git://)")
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def parse_source(source: str) -> tuple[PackageSource, str]:
|
|
36
|
+
"""Parse source string and determine type.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
source: Source string (e.g., "owner/repo", "https://...", "/path/to/skill")
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Tuple of (PackageSource, normalized_source)
|
|
43
|
+
"""
|
|
44
|
+
source = source.strip()
|
|
45
|
+
|
|
46
|
+
# Check if it's a GitHub shorthand (owner/repo)
|
|
47
|
+
if PackageSourceHandler.GITHUB_PATTERN.match(source):
|
|
48
|
+
return PackageSource.GITHUB, source
|
|
49
|
+
|
|
50
|
+
# Check if it's a Git URL
|
|
51
|
+
if PackageSourceHandler.GIT_URL_PATTERN.match(source):
|
|
52
|
+
return PackageSource.GIT_URL, source
|
|
53
|
+
|
|
54
|
+
# Check if it's a local path
|
|
55
|
+
path = Path(source).expanduser().resolve()
|
|
56
|
+
if path.exists():
|
|
57
|
+
return PackageSource.LOCAL, str(path)
|
|
58
|
+
|
|
59
|
+
return PackageSource.UNKNOWN, source
|
|
60
|
+
|
|
61
|
+
@staticmethod
|
|
62
|
+
def fetch(source: str, source_type: PackageSource, temp_dir: Path) -> Path:
|
|
63
|
+
"""Fetch package source to temporary directory.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
source: Source string
|
|
67
|
+
source_type: Type of source
|
|
68
|
+
temp_dir: Temporary directory to fetch to
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Path to fetched package directory
|
|
72
|
+
"""
|
|
73
|
+
if source_type == PackageSource.LOCAL:
|
|
74
|
+
source_path = Path(source)
|
|
75
|
+
if not source_path.exists():
|
|
76
|
+
raise PackageManagerError(f"Local path does not exist: {source}")
|
|
77
|
+
# Copy local directory to temp
|
|
78
|
+
dest = temp_dir / source_path.name
|
|
79
|
+
shutil.copytree(source_path, dest, symlinks=False)
|
|
80
|
+
return dest
|
|
81
|
+
|
|
82
|
+
elif source_type == PackageSource.GITHUB:
|
|
83
|
+
# Convert GitHub shorthand to full URL
|
|
84
|
+
git_url = f"https://github.com/{source}.git"
|
|
85
|
+
return PackageSourceHandler._git_clone(git_url, temp_dir)
|
|
86
|
+
|
|
87
|
+
elif source_type == PackageSource.GIT_URL:
|
|
88
|
+
return PackageSourceHandler._git_clone(source, temp_dir)
|
|
89
|
+
|
|
90
|
+
else:
|
|
91
|
+
raise PackageManagerError(f"Unsupported source type: {source_type}")
|
|
92
|
+
|
|
93
|
+
@staticmethod
|
|
94
|
+
def _git_clone(url: str, temp_dir: Path) -> Path:
|
|
95
|
+
"""Clone a git repository.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
url: Git URL
|
|
99
|
+
temp_dir: Directory to clone into
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
Path to cloned repository
|
|
103
|
+
"""
|
|
104
|
+
import subprocess
|
|
105
|
+
|
|
106
|
+
# Extract repo name from URL
|
|
107
|
+
repo_name = url.rstrip("/").split("/")[-1]
|
|
108
|
+
if repo_name.endswith(".git"):
|
|
109
|
+
repo_name = repo_name[:-4]
|
|
110
|
+
|
|
111
|
+
dest = temp_dir / repo_name
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
subprocess.run(
|
|
115
|
+
["git", "clone", "--depth", "1", url, str(dest)],
|
|
116
|
+
check=True,
|
|
117
|
+
capture_output=True,
|
|
118
|
+
text=True,
|
|
119
|
+
)
|
|
120
|
+
except subprocess.CalledProcessError as e:
|
|
121
|
+
raise PackageManagerError(
|
|
122
|
+
f"Failed to clone repository: {e.stderr or e.stdout}"
|
|
123
|
+
)
|
|
124
|
+
except FileNotFoundError:
|
|
125
|
+
raise PackageManagerError(
|
|
126
|
+
"git command not found. Please install git to use git sources."
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
return dest
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class PackageManager:
|
|
133
|
+
"""Manages installation and lifecycle of UACS skill packages.
|
|
134
|
+
|
|
135
|
+
Packages are stored in .agent/skills/ directory with metadata in
|
|
136
|
+
.agent/skills/.packages.json.
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
def __init__(self, base_path: Path | None = None):
|
|
140
|
+
"""Initialize package manager.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
base_path: Base path for package storage. Defaults to current directory.
|
|
144
|
+
"""
|
|
145
|
+
self.base_path = base_path or Path.cwd()
|
|
146
|
+
self.skills_dir = self.base_path / ".agent" / "skills"
|
|
147
|
+
self.metadata_file = self.skills_dir / ".packages.json"
|
|
148
|
+
self.validator = SkillValidator()
|
|
149
|
+
|
|
150
|
+
# Ensure directories exist
|
|
151
|
+
self.skills_dir.mkdir(parents=True, exist_ok=True)
|
|
152
|
+
|
|
153
|
+
def _load_metadata(self) -> dict[str, Any]:
|
|
154
|
+
"""Load package metadata from .packages.json."""
|
|
155
|
+
if not self.metadata_file.exists():
|
|
156
|
+
return {"packages": {}}
|
|
157
|
+
|
|
158
|
+
try:
|
|
159
|
+
with open(self.metadata_file, "r", encoding="utf-8") as f:
|
|
160
|
+
return json.load(f)
|
|
161
|
+
except json.JSONDecodeError as e:
|
|
162
|
+
raise PackageManagerError(f"Failed to parse metadata file: {e}")
|
|
163
|
+
|
|
164
|
+
def _save_metadata(self, metadata: dict[str, Any]) -> None:
|
|
165
|
+
"""Save package metadata to .packages.json."""
|
|
166
|
+
with open(self.metadata_file, "w", encoding="utf-8") as f:
|
|
167
|
+
json.dump(metadata, f, indent=2, ensure_ascii=False)
|
|
168
|
+
|
|
169
|
+
def install(self, source: str, validate: bool = True, force: bool = False) -> InstalledPackage:
|
|
170
|
+
"""Install a package from a source.
|
|
171
|
+
|
|
172
|
+
Installation flow:
|
|
173
|
+
1. Parse source to determine type
|
|
174
|
+
2. Fetch to temporary directory
|
|
175
|
+
3. Validate using SkillValidator (if validate=True)
|
|
176
|
+
4. Copy to .agent/skills/{name}/
|
|
177
|
+
5. Save metadata
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
source: Package source (GitHub repo, Git URL, or local path)
|
|
181
|
+
validate: Whether to validate before installing (default: True)
|
|
182
|
+
force: Whether to overwrite existing package (default: False)
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
InstalledPackage with installation details
|
|
186
|
+
|
|
187
|
+
Raises:
|
|
188
|
+
PackageManagerError: If installation fails
|
|
189
|
+
"""
|
|
190
|
+
# Parse source
|
|
191
|
+
source_type, normalized_source = PackageSourceHandler.parse_source(source)
|
|
192
|
+
|
|
193
|
+
if source_type == PackageSource.UNKNOWN:
|
|
194
|
+
raise PackageManagerError(
|
|
195
|
+
f"Unable to determine package source type: {source}\n"
|
|
196
|
+
"Supported formats:\n"
|
|
197
|
+
" - GitHub: owner/repo\n"
|
|
198
|
+
" - Git URL: https://... or git@...\n"
|
|
199
|
+
" - Local path: /path/to/skill"
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
# Create temporary directory for fetching
|
|
203
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
204
|
+
temp_path = Path(temp_dir)
|
|
205
|
+
|
|
206
|
+
# Fetch package to temp directory
|
|
207
|
+
try:
|
|
208
|
+
fetched_path = PackageSourceHandler.fetch(
|
|
209
|
+
normalized_source, source_type, temp_path
|
|
210
|
+
)
|
|
211
|
+
except Exception as e:
|
|
212
|
+
raise PackageManagerError(f"Failed to fetch package: {e}")
|
|
213
|
+
|
|
214
|
+
# Validate package if requested
|
|
215
|
+
if validate:
|
|
216
|
+
validation_result = self.validator.validate_file(fetched_path)
|
|
217
|
+
|
|
218
|
+
if not validation_result.valid:
|
|
219
|
+
error_messages = [
|
|
220
|
+
f"{err.field}: {err.message}" for err in validation_result.errors
|
|
221
|
+
]
|
|
222
|
+
raise PackageManagerError(
|
|
223
|
+
f"Package validation failed:\n" + "\n".join(error_messages)
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
# Extract package name from metadata
|
|
227
|
+
if not validation_result.metadata or "name" not in validation_result.metadata:
|
|
228
|
+
raise PackageManagerError(
|
|
229
|
+
"Package validation succeeded but no name found in metadata"
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
package_name = validation_result.metadata["name"]
|
|
233
|
+
package_metadata = validation_result.metadata
|
|
234
|
+
else:
|
|
235
|
+
# Try to extract name from SKILL.md manually
|
|
236
|
+
skill_md = fetched_path / "SKILL.md"
|
|
237
|
+
if skill_md.exists():
|
|
238
|
+
import re
|
|
239
|
+
content = skill_md.read_text()
|
|
240
|
+
match = re.search(r'^name:\s*(.+)$', content, re.MULTILINE)
|
|
241
|
+
if match:
|
|
242
|
+
package_name = match.group(1).strip()
|
|
243
|
+
package_metadata = {"name": package_name}
|
|
244
|
+
else:
|
|
245
|
+
raise PackageManagerError(
|
|
246
|
+
"Could not determine package name. Enable validation or ensure SKILL.md has a 'name' field."
|
|
247
|
+
)
|
|
248
|
+
else:
|
|
249
|
+
raise PackageManagerError(
|
|
250
|
+
"No SKILL.md found. Enable validation to install this package."
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
# Check if already installed
|
|
254
|
+
metadata = self._load_metadata()
|
|
255
|
+
if package_name in metadata.get("packages", {}):
|
|
256
|
+
if not force:
|
|
257
|
+
raise PackageManagerError(
|
|
258
|
+
f"Package '{package_name}' is already installed. "
|
|
259
|
+
f"Use force=True to overwrite or update() to update it."
|
|
260
|
+
)
|
|
261
|
+
# Remove existing package if force=True
|
|
262
|
+
self.uninstall(package_name)
|
|
263
|
+
|
|
264
|
+
# Copy to skills directory
|
|
265
|
+
dest_path = self.skills_dir / package_name
|
|
266
|
+
if dest_path.exists():
|
|
267
|
+
raise PackageManagerError(
|
|
268
|
+
f"Directory already exists: {dest_path}. "
|
|
269
|
+
f"Remove it before installing."
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
try:
|
|
273
|
+
shutil.copytree(fetched_path, dest_path, symlinks=False)
|
|
274
|
+
except Exception as e:
|
|
275
|
+
raise PackageManagerError(f"Failed to copy package: {e}")
|
|
276
|
+
|
|
277
|
+
# Create installed package record
|
|
278
|
+
installed_pkg = InstalledPackage(
|
|
279
|
+
name=package_name,
|
|
280
|
+
source=source,
|
|
281
|
+
source_type=source_type,
|
|
282
|
+
version=package_metadata.get("version"),
|
|
283
|
+
location=dest_path,
|
|
284
|
+
is_valid=True,
|
|
285
|
+
validation_errors=[],
|
|
286
|
+
metadata=package_metadata,
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
# Save to metadata
|
|
290
|
+
metadata["packages"][package_name] = installed_pkg.to_dict()
|
|
291
|
+
self._save_metadata(metadata)
|
|
292
|
+
|
|
293
|
+
return installed_pkg
|
|
294
|
+
|
|
295
|
+
def list_installed(self) -> list[InstalledPackage]:
|
|
296
|
+
"""List all installed packages.
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
List of InstalledPackage objects
|
|
300
|
+
"""
|
|
301
|
+
metadata = self._load_metadata()
|
|
302
|
+
packages = []
|
|
303
|
+
|
|
304
|
+
for pkg_data in metadata.get("packages", {}).values():
|
|
305
|
+
try:
|
|
306
|
+
packages.append(InstalledPackage.from_dict(pkg_data))
|
|
307
|
+
except Exception:
|
|
308
|
+
# Skip malformed entries
|
|
309
|
+
continue
|
|
310
|
+
|
|
311
|
+
return packages
|
|
312
|
+
|
|
313
|
+
def validate(self, package_name: str) -> ValidationResult:
|
|
314
|
+
"""Validate an installed package.
|
|
315
|
+
|
|
316
|
+
Args:
|
|
317
|
+
package_name: Name of package to validate
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
ValidationResult
|
|
321
|
+
|
|
322
|
+
Raises:
|
|
323
|
+
PackageManagerError: If package not found
|
|
324
|
+
"""
|
|
325
|
+
metadata = self._load_metadata()
|
|
326
|
+
packages = metadata.get("packages", {})
|
|
327
|
+
|
|
328
|
+
if package_name not in packages:
|
|
329
|
+
raise PackageManagerError(f"Package not found: {package_name}")
|
|
330
|
+
|
|
331
|
+
pkg_data = packages[package_name]
|
|
332
|
+
location = Path(pkg_data["location"]) if pkg_data.get("location") else None
|
|
333
|
+
|
|
334
|
+
if not location or not location.exists():
|
|
335
|
+
raise PackageManagerError(
|
|
336
|
+
f"Package directory not found: {location}. Package may be corrupted."
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
return self.validator.validate_file(location)
|
|
340
|
+
|
|
341
|
+
def uninstall(self, package_name: str) -> bool:
|
|
342
|
+
"""Uninstall a package.
|
|
343
|
+
|
|
344
|
+
Args:
|
|
345
|
+
package_name: Name of package to uninstall
|
|
346
|
+
|
|
347
|
+
Returns:
|
|
348
|
+
True if successful
|
|
349
|
+
|
|
350
|
+
Raises:
|
|
351
|
+
PackageManagerError: If uninstallation fails
|
|
352
|
+
"""
|
|
353
|
+
metadata = self._load_metadata()
|
|
354
|
+
packages = metadata.get("packages", {})
|
|
355
|
+
|
|
356
|
+
if package_name not in packages:
|
|
357
|
+
raise PackageManagerError(f"Package not found: {package_name}")
|
|
358
|
+
|
|
359
|
+
# Get package location
|
|
360
|
+
pkg_data = packages[package_name]
|
|
361
|
+
location = Path(pkg_data["location"]) if pkg_data.get("location") else None
|
|
362
|
+
|
|
363
|
+
# Remove directory
|
|
364
|
+
if location and location.exists():
|
|
365
|
+
try:
|
|
366
|
+
shutil.rmtree(location)
|
|
367
|
+
except Exception as e:
|
|
368
|
+
raise PackageManagerError(
|
|
369
|
+
f"Failed to remove package directory: {e}"
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
# Remove from metadata
|
|
373
|
+
del packages[package_name]
|
|
374
|
+
self._save_metadata(metadata)
|
|
375
|
+
|
|
376
|
+
return True
|
|
377
|
+
|
|
378
|
+
def update(self, package_name: str) -> InstalledPackage:
|
|
379
|
+
"""Update an installed package.
|
|
380
|
+
|
|
381
|
+
Args:
|
|
382
|
+
package_name: Name of package to update
|
|
383
|
+
|
|
384
|
+
Returns:
|
|
385
|
+
Updated InstalledPackage
|
|
386
|
+
|
|
387
|
+
Raises:
|
|
388
|
+
PackageManagerError: If update fails
|
|
389
|
+
"""
|
|
390
|
+
metadata = self._load_metadata()
|
|
391
|
+
packages = metadata.get("packages", {})
|
|
392
|
+
|
|
393
|
+
if package_name not in packages:
|
|
394
|
+
raise PackageManagerError(f"Package not found: {package_name}")
|
|
395
|
+
|
|
396
|
+
pkg_data = packages[package_name]
|
|
397
|
+
source = pkg_data["source"]
|
|
398
|
+
source_type = PackageSource(pkg_data["source_type"])
|
|
399
|
+
|
|
400
|
+
# For local sources, we can't update
|
|
401
|
+
if source_type == PackageSource.LOCAL:
|
|
402
|
+
raise PackageManagerError(
|
|
403
|
+
"Cannot update local packages. Use uninstall and install instead."
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
# Uninstall existing
|
|
407
|
+
self.uninstall(package_name)
|
|
408
|
+
|
|
409
|
+
# Reinstall from source
|
|
410
|
+
try:
|
|
411
|
+
return self.install(source)
|
|
412
|
+
except Exception as e:
|
|
413
|
+
raise PackageManagerError(f"Failed to update package: {e}")
|
uacs/packages/models.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Data models for package management."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PackageSource(Enum):
|
|
11
|
+
"""Source type for package installation."""
|
|
12
|
+
|
|
13
|
+
GITHUB = "github" # owner/repo format
|
|
14
|
+
GIT_URL = "git_url" # Full git URL
|
|
15
|
+
LOCAL = "local" # Local filesystem path
|
|
16
|
+
UNKNOWN = "unknown"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class InstalledPackage:
|
|
21
|
+
"""Represents an installed package in .agent/skills/"""
|
|
22
|
+
|
|
23
|
+
name: str
|
|
24
|
+
source: str # Original source string (e.g., "owner/repo")
|
|
25
|
+
source_type: PackageSource
|
|
26
|
+
version: str | None = None
|
|
27
|
+
install_date: datetime = field(default_factory=datetime.now)
|
|
28
|
+
location: Path | None = None
|
|
29
|
+
is_valid: bool = True
|
|
30
|
+
validation_errors: list[str] = field(default_factory=list)
|
|
31
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
32
|
+
|
|
33
|
+
def to_dict(self) -> dict[str, Any]:
|
|
34
|
+
"""Convert to dictionary."""
|
|
35
|
+
return {
|
|
36
|
+
"name": self.name,
|
|
37
|
+
"source": self.source,
|
|
38
|
+
"source_type": self.source_type.value,
|
|
39
|
+
"version": self.version,
|
|
40
|
+
"install_date": self.install_date.isoformat(),
|
|
41
|
+
"location": str(self.location) if self.location else None,
|
|
42
|
+
"is_valid": self.is_valid,
|
|
43
|
+
"validation_errors": self.validation_errors,
|
|
44
|
+
"metadata": self.metadata,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def from_dict(cls, data: dict[str, Any]) -> "InstalledPackage":
|
|
49
|
+
"""Create from dictionary."""
|
|
50
|
+
return cls(
|
|
51
|
+
name=data["name"],
|
|
52
|
+
source=data["source"],
|
|
53
|
+
source_type=PackageSource(data["source_type"]),
|
|
54
|
+
version=data.get("version"),
|
|
55
|
+
install_date=datetime.fromisoformat(data["install_date"]),
|
|
56
|
+
location=Path(data["location"]) if data.get("location") else None,
|
|
57
|
+
is_valid=data.get("is_valid", True),
|
|
58
|
+
validation_errors=data.get("validation_errors", []),
|
|
59
|
+
metadata=data.get("metadata", {}),
|
|
60
|
+
)
|