cognitive-modules 0.3.0__py3-none-any.whl → 0.5.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.
cognitive/registry.py CHANGED
@@ -17,8 +17,11 @@ import subprocess
17
17
  import tempfile
18
18
  from pathlib import Path
19
19
  from typing import Optional
20
- from urllib.request import urlopen
20
+ from urllib.request import urlopen, Request
21
21
  from urllib.error import URLError
22
+ import zipfile
23
+ import io
24
+ import re
22
25
 
23
26
  # Standard module search paths
24
27
  SEARCH_PATHS = [
@@ -54,9 +57,11 @@ def find_module(name: str) -> Optional[Path]:
54
57
  """Find a module by name, searching all paths in order."""
55
58
  for base_path in get_search_paths():
56
59
  module_path = base_path / name
57
- # Support both new and old format
60
+ # Support v2, v1, and v0 formats
58
61
  if module_path.exists():
59
- if (module_path / "MODULE.md").exists() or (module_path / "module.md").exists():
62
+ if (module_path / "module.yaml").exists() or \
63
+ (module_path / "MODULE.md").exists() or \
64
+ (module_path / "module.md").exists():
60
65
  return module_path
61
66
  return None
62
67
 
@@ -75,12 +80,16 @@ def list_modules() -> list[dict]:
75
80
  continue
76
81
  if module_dir.name in seen:
77
82
  continue
78
- # Support both formats
79
- if not ((module_dir / "MODULE.md").exists() or (module_dir / "module.md").exists()):
80
- continue
81
83
 
82
- # Detect format
83
- fmt = "new" if (module_dir / "MODULE.md").exists() else "old"
84
+ # Detect format: v2, v1, or v0
85
+ if (module_dir / "module.yaml").exists():
86
+ fmt = "v2"
87
+ elif (module_dir / "MODULE.md").exists():
88
+ fmt = "v1"
89
+ elif (module_dir / "module.md").exists():
90
+ fmt = "v0"
91
+ else:
92
+ continue
84
93
 
85
94
  seen.add(module_dir.name)
86
95
  modules.append({
@@ -105,9 +114,9 @@ def install_from_local(source: Path, name: Optional[str] = None) -> Path:
105
114
  if not source.exists():
106
115
  raise FileNotFoundError(f"Source not found: {source}")
107
116
 
108
- # Check for valid module (either format)
109
- if not ((source / "MODULE.md").exists() or (source / "module.md").exists()):
110
- raise ValueError(f"Not a valid module (missing MODULE.md or module.md): {source}")
117
+ # Check for valid module (v2, v1, or v0 format)
118
+ if not _is_valid_module(source):
119
+ raise ValueError(f"Not a valid module (missing module.yaml, MODULE.md, or module.md): {source}")
111
120
 
112
121
  module_name = name or source.name
113
122
  target = ensure_user_modules_dir() / module_name
@@ -116,9 +125,277 @@ def install_from_local(source: Path, name: Optional[str] = None) -> Path:
116
125
  shutil.rmtree(target)
117
126
 
118
127
  shutil.copytree(source, target)
128
+
129
+ # Record source info for update tracking
130
+ _record_module_source(module_name, source)
131
+
119
132
  return target
120
133
 
121
134
 
135
+ def _record_module_source(
136
+ name: str,
137
+ source: Path,
138
+ github_url: str = None,
139
+ module_path: str = None,
140
+ tag: str = None,
141
+ branch: str = None,
142
+ version: str = None,
143
+ ):
144
+ """Record module source info for future updates."""
145
+ manifest_path = Path.home() / ".cognitive" / "installed.json"
146
+ manifest_path.parent.mkdir(parents=True, exist_ok=True)
147
+
148
+ # Load existing manifest
149
+ manifest = {}
150
+ if manifest_path.exists():
151
+ try:
152
+ with open(manifest_path, 'r') as f:
153
+ manifest = json.load(f)
154
+ except:
155
+ pass
156
+
157
+ # Get current timestamp
158
+ from datetime import datetime
159
+ now = datetime.now().isoformat()
160
+
161
+ # Update entry
162
+ manifest[name] = {
163
+ "source": str(source),
164
+ "github_url": github_url,
165
+ "module_path": module_path,
166
+ "tag": tag,
167
+ "branch": branch,
168
+ "version": version,
169
+ "installed_at": str(Path.home() / ".cognitive" / "modules" / name),
170
+ "installed_time": now,
171
+ }
172
+
173
+ with open(manifest_path, 'w') as f:
174
+ json.dump(manifest, f, indent=2)
175
+
176
+
177
+ def get_installed_module_info(name: str) -> Optional[dict]:
178
+ """Get installation info for a module."""
179
+ manifest_path = Path.home() / ".cognitive" / "installed.json"
180
+ if not manifest_path.exists():
181
+ return None
182
+
183
+ try:
184
+ with open(manifest_path, 'r') as f:
185
+ manifest = json.load(f)
186
+ return manifest.get(name)
187
+ except:
188
+ return None
189
+
190
+
191
+ def get_module_version(module_path: Path) -> Optional[str]:
192
+ """Extract version from a module's metadata."""
193
+ import yaml
194
+
195
+ # Try v2 format (module.yaml)
196
+ yaml_path = module_path / "module.yaml"
197
+ if yaml_path.exists():
198
+ try:
199
+ with open(yaml_path, 'r') as f:
200
+ data = yaml.safe_load(f)
201
+ return data.get("version")
202
+ except:
203
+ pass
204
+
205
+ # Try v1 format (MODULE.md with frontmatter)
206
+ md_path = module_path / "MODULE.md"
207
+ if not md_path.exists():
208
+ md_path = module_path / "module.md"
209
+
210
+ if md_path.exists():
211
+ try:
212
+ with open(md_path, 'r') as f:
213
+ content = f.read()
214
+ if content.startswith("---"):
215
+ parts = content.split("---", 2)
216
+ if len(parts) >= 3:
217
+ meta = yaml.safe_load(parts[1])
218
+ return meta.get("version")
219
+ except:
220
+ pass
221
+
222
+ return None
223
+
224
+
225
+ def update_module(name: str) -> tuple[Path, str, str]:
226
+ """
227
+ Update an installed module to the latest version.
228
+
229
+ Returns: (path, old_version, new_version)
230
+ """
231
+ info = get_installed_module_info(name)
232
+ if not info:
233
+ raise ValueError(f"Module not installed or no source info: {name}")
234
+
235
+ github_url = info.get("github_url")
236
+ if not github_url:
237
+ raise ValueError(f"Module was not installed from GitHub, cannot update: {name}")
238
+
239
+ # Get current version
240
+ current_path = USER_MODULES_DIR / name
241
+ old_version = get_module_version(current_path) if current_path.exists() else None
242
+
243
+ # Re-install from source
244
+ module_path = info.get("module_path")
245
+ tag = info.get("tag")
246
+ branch = info.get("branch", "main")
247
+
248
+ # If installed with a specific tag, use that tag; otherwise use branch
249
+ ref = tag if tag else branch
250
+
251
+ new_path = install_from_github_url(
252
+ url=github_url,
253
+ module_path=module_path,
254
+ name=name,
255
+ tag=tag,
256
+ branch=branch if not tag else "main",
257
+ )
258
+
259
+ # Get new version
260
+ new_version = get_module_version(new_path)
261
+
262
+ return new_path, old_version, new_version
263
+
264
+
265
+ def install_from_github_url(
266
+ url: str,
267
+ module_path: Optional[str] = None,
268
+ name: Optional[str] = None,
269
+ branch: str = "main",
270
+ tag: Optional[str] = None,
271
+ ) -> Path:
272
+ """
273
+ Install a module from a GitHub URL without requiring git.
274
+
275
+ Uses GitHub's ZIP download feature for lightweight installation.
276
+
277
+ Args:
278
+ url: GitHub URL or org/repo shorthand
279
+ module_path: Path to module within repository
280
+ name: Override module name
281
+ branch: Git branch (default: main)
282
+ tag: Git tag/release version (takes priority over branch)
283
+
284
+ Examples:
285
+ install_from_github_url("https://github.com/ziel-io/cognitive-modules",
286
+ module_path="cognitive/modules/code-simplifier")
287
+ install_from_github_url("ziel-io/cognitive-modules",
288
+ module_path="code-simplifier",
289
+ tag="v1.0.0")
290
+ """
291
+ # Parse shorthand (org/repo)
292
+ original_url = url
293
+ if not url.startswith("http"):
294
+ url = f"https://github.com/{url}"
295
+
296
+ # Extract org/repo from URL
297
+ match = re.match(r"https://github\.com/([^/]+)/([^/]+)/?", url)
298
+ if not match:
299
+ raise ValueError(f"Invalid GitHub URL: {url}")
300
+
301
+ org, repo = match.groups()
302
+ repo = repo.rstrip(".git")
303
+ github_url = f"https://github.com/{org}/{repo}"
304
+
305
+ # Build ZIP download URL (tag takes priority over branch)
306
+ if tag:
307
+ # Try tag first, then as a release tag
308
+ zip_url = f"https://github.com/{org}/{repo}/archive/refs/tags/{tag}.zip"
309
+ else:
310
+ zip_url = f"https://github.com/{org}/{repo}/archive/refs/heads/{branch}.zip"
311
+
312
+ try:
313
+ # Download ZIP
314
+ req = Request(zip_url, headers={"User-Agent": "cognitive-modules/1.0"})
315
+ with urlopen(req, timeout=30) as response:
316
+ zip_data = response.read()
317
+ except URLError as e:
318
+ if tag:
319
+ raise RuntimeError(f"Failed to download tag '{tag}' from GitHub: {e}")
320
+ raise RuntimeError(f"Failed to download from GitHub: {e}")
321
+
322
+ # Extract to temp directory
323
+ with tempfile.TemporaryDirectory() as tmpdir:
324
+ tmppath = Path(tmpdir)
325
+
326
+ # Extract ZIP
327
+ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
328
+ zf.extractall(tmppath)
329
+
330
+ # Find extracted directory (usually repo-branch or repo-tag)
331
+ extracted_dirs = list(tmppath.iterdir())
332
+ if not extracted_dirs:
333
+ raise RuntimeError("ZIP file was empty")
334
+ repo_root = extracted_dirs[0]
335
+
336
+ # Determine source path
337
+ if module_path:
338
+ # Try different module path patterns
339
+ source = None
340
+ possible_paths = [
341
+ repo_root / module_path,
342
+ repo_root / "cognitive" / "modules" / module_path,
343
+ repo_root / "modules" / module_path,
344
+ ]
345
+ for p in possible_paths:
346
+ if p.exists() and _is_valid_module(p):
347
+ source = p
348
+ break
349
+
350
+ if not source:
351
+ raise FileNotFoundError(
352
+ f"Module not found at: {module_path}\n"
353
+ f"Searched in: {[str(p.relative_to(repo_root)) for p in possible_paths]}"
354
+ )
355
+ else:
356
+ # Use repo root as module
357
+ source = repo_root
358
+ if not _is_valid_module(source):
359
+ raise ValueError(
360
+ f"Repository root is not a valid module. "
361
+ f"Use --module to specify the module path."
362
+ )
363
+
364
+ # Determine module name and version
365
+ module_name = name or source.name
366
+ version = get_module_version(source)
367
+
368
+ # Install to user modules dir
369
+ target = ensure_user_modules_dir() / module_name
370
+
371
+ if target.exists():
372
+ shutil.rmtree(target)
373
+
374
+ shutil.copytree(source, target)
375
+
376
+ # Record source info for future updates
377
+ _record_module_source(
378
+ name=module_name,
379
+ source=source,
380
+ github_url=github_url,
381
+ module_path=module_path,
382
+ tag=tag,
383
+ branch=branch,
384
+ version=version,
385
+ )
386
+
387
+ return target
388
+
389
+
390
+ def _is_valid_module(path: Path) -> bool:
391
+ """Check if a directory is a valid cognitive module."""
392
+ return (
393
+ (path / "module.yaml").exists() or # v2 format
394
+ (path / "MODULE.md").exists() or # v1 format
395
+ (path / "module.md").exists() # v0 format
396
+ )
397
+
398
+
122
399
  def install_from_git(url: str, subdir: Optional[str] = None, name: Optional[str] = None) -> Path:
123
400
  """
124
401
  Install a module from a git repository.
@@ -274,3 +551,40 @@ def search_registry(query: str) -> list[dict]:
274
551
  })
275
552
 
276
553
  return results
554
+
555
+
556
+ def list_github_tags(url: str, limit: int = 10) -> list[str]:
557
+ """
558
+ List available tags/releases from a GitHub repository.
559
+
560
+ Args:
561
+ url: GitHub URL or org/repo shorthand
562
+ limit: Maximum number of tags to return
563
+
564
+ Returns:
565
+ List of tag names (e.g., ["v1.2.0", "v1.1.0", "v1.0.0"])
566
+ """
567
+ # Parse shorthand
568
+ if not url.startswith("http"):
569
+ url = f"https://github.com/{url}"
570
+
571
+ match = re.match(r"https://github\.com/([^/]+)/([^/]+)/?", url)
572
+ if not match:
573
+ raise ValueError(f"Invalid GitHub URL: {url}")
574
+
575
+ org, repo = match.groups()
576
+ repo = repo.rstrip(".git")
577
+
578
+ # Use GitHub API to get tags
579
+ api_url = f"https://api.github.com/repos/{org}/{repo}/tags?per_page={limit}"
580
+
581
+ try:
582
+ req = Request(api_url, headers={
583
+ "User-Agent": "cognitive-modules/1.0",
584
+ "Accept": "application/vnd.github.v3+json",
585
+ })
586
+ with urlopen(req, timeout=10) as response:
587
+ data = json.loads(response.read().decode())
588
+ return [tag["name"] for tag in data]
589
+ except URLError as e:
590
+ return []