Skip to content

Commit a6b1574

Browse files
Ki-SekiCopilot
andauthored
feat: add download_examples command (#87)
* fix: examples cannot be used while using pip Fixes #52 * docs: update README * fix: revert deletion * test: add unit tests for MemOS CLI functions * Update Makefile Co-authored-by: Copilot <[email protected]> --------- Co-authored-by: Copilot <[email protected]>
1 parent b7636eb commit a6b1574

File tree

6 files changed

+231
-17
lines changed

6 files changed

+231
-17
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,4 @@ serve:
2424
poetry run uvicorn memos.api.start_api:app
2525

2626
openapi:
27-
poetry run python scripts/export_openapi.py --output docs/openapi.json
27+
poetry run memos export_openapi --output docs/openapi.json

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,14 @@ curl -fsSL https://ollama.com/install.sh | sh
174174

175175
To use functionalities based on the `transformers` library, ensure you have [PyTorch](https://pytorch.org/get-started/locally/) installed (CUDA version recommended for GPU acceleration).
176176

177+
#### Download Examples
178+
179+
To download example code, data and configurations, run the following command:
180+
181+
```bash
182+
memos download_examples
183+
```
184+
177185
## 💬 Community & Support
178186

179187
Join our community to ask questions, share your projects, and connect with other developers.

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ python-dotenv = "^1.1.1"
5959
langgraph = "^0.5.1"
6060
langmem = "^0.0.27"
6161

62+
[tool.poetry.scripts]
63+
memos = "memos.cli:main"
64+
6265
[[tool.poetry.source]]
6366
name = "mirrors"
6467
url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple/"

scripts/export_openapi.py

Lines changed: 0 additions & 16 deletions
This file was deleted.

src/memos/cli.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
"""
2+
MemOS CLI Tool
3+
This script provides command-line interface for MemOS operations.
4+
"""
5+
6+
import argparse
7+
import json
8+
import os
9+
import zipfile
10+
11+
from io import BytesIO
12+
13+
14+
def export_openapi(output: str) -> bool:
15+
"""Export OpenAPI schema to JSON file."""
16+
from memos.api.start_api import app
17+
18+
# Create directory if it doesn't exist
19+
if os.path.dirname(output):
20+
os.makedirs(os.path.dirname(output), exist_ok=True)
21+
22+
with open(output, "w") as f:
23+
json.dump(app.openapi(), f, indent=2)
24+
f.write("\n")
25+
26+
print(f"✅ OpenAPI schema exported to: {output}")
27+
return True
28+
29+
30+
def download_examples(dest: str) -> bool:
31+
import requests
32+
33+
"""Download examples from the MemOS repository."""
34+
zip_url = "https://github.com/MemTensor/MemOS/archive/refs/heads/main.zip"
35+
print(f"📥 Downloading examples from {zip_url}...")
36+
37+
try:
38+
response = requests.get(zip_url)
39+
response.raise_for_status()
40+
41+
with zipfile.ZipFile(BytesIO(response.content)) as z:
42+
extracted_files = []
43+
for file in z.namelist():
44+
if "MemOS-main/examples/" in file and not file.endswith("/"):
45+
# Remove the prefix and extract to dest
46+
relative_path = file.replace("MemOS-main/examples/", "")
47+
extract_path = os.path.join(dest, relative_path)
48+
49+
# Create directory if it doesn't exist
50+
os.makedirs(os.path.dirname(extract_path), exist_ok=True)
51+
52+
# Extract the file
53+
with z.open(file) as source, open(extract_path, "wb") as target:
54+
target.write(source.read())
55+
extracted_files.append(extract_path)
56+
57+
print(f"✅ Examples downloaded to: {dest}")
58+
print(f"📁 {len(extracted_files)} files extracted")
59+
60+
except requests.RequestException as e:
61+
print(f"❌ Error downloading examples: {e}")
62+
return False
63+
except Exception as e:
64+
print(f"❌ Error extracting examples: {e}")
65+
return False
66+
67+
return True
68+
69+
70+
def main():
71+
"""Main CLI entry point."""
72+
parser = argparse.ArgumentParser(
73+
prog="memos",
74+
description="MemOS Command Line Interface",
75+
)
76+
77+
# Create subparsers for different commands
78+
subparsers = parser.add_subparsers(dest="command", help="Available commands")
79+
80+
# Download examples command
81+
examples_parser = subparsers.add_parser("download_examples", help="Download example files")
82+
examples_parser.add_argument(
83+
"--dest",
84+
type=str,
85+
default="./examples",
86+
help="Destination directory for examples (default: ./examples)",
87+
)
88+
89+
# Export API command
90+
api_parser = subparsers.add_parser("export_openapi", help="Export OpenAPI schema to JSON file")
91+
api_parser.add_argument(
92+
"--output",
93+
type=str,
94+
default="openapi.json",
95+
help="Output path for OpenAPI schema (default: openapi.json)",
96+
)
97+
98+
# Parse arguments
99+
args = parser.parse_args()
100+
101+
# Handle commands
102+
if args.command == "download_examples":
103+
success = download_examples(args.dest)
104+
exit(0 if success else 1)
105+
elif args.command == "export_openapi":
106+
success = export_openapi(args.output)
107+
exit(0 if success else 1)
108+
else:
109+
parser.print_help()
110+
111+
112+
if __name__ == "__main__":
113+
main()

tests/test_cli.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
"""
2+
Tests for the MemOS CLI tool.
3+
"""
4+
5+
import zipfile
6+
7+
from io import BytesIO
8+
from unittest.mock import MagicMock, mock_open, patch
9+
10+
import pytest
11+
import requests
12+
13+
from memos.cli import download_examples, export_openapi, main
14+
15+
16+
class TestExportOpenAPI:
17+
"""Test the export_openapi function."""
18+
19+
@patch("memos.api.start_api.app")
20+
@patch("builtins.open", new_callable=mock_open)
21+
@patch("os.makedirs")
22+
def test_export_openapi_success(self, mock_makedirs, mock_file, mock_app):
23+
"""Test successful OpenAPI export."""
24+
mock_openapi_data = {"openapi": "3.0.0", "info": {"title": "Test API"}}
25+
mock_app.openapi.return_value = mock_openapi_data
26+
27+
result = export_openapi("/test/path/openapi.json")
28+
29+
assert result is True
30+
mock_makedirs.assert_called_once_with("/test/path", exist_ok=True)
31+
mock_file.assert_called_once_with("/test/path/openapi.json", "w")
32+
33+
@patch("memos.api.start_api.app")
34+
@patch("builtins.open", side_effect=OSError("Permission denied"))
35+
def test_export_openapi_error(self, mock_file, mock_app):
36+
"""Test OpenAPI export when file writing fails."""
37+
mock_app.openapi.return_value = {"test": "data"}
38+
39+
with pytest.raises(IOError):
40+
export_openapi("/invalid/path/openapi.json")
41+
42+
43+
class TestDownloadExamples:
44+
"""Test the download_examples function."""
45+
46+
def create_mock_zip_content(self):
47+
"""Create mock zip file content for testing."""
48+
zip_buffer = BytesIO()
49+
with zipfile.ZipFile(zip_buffer, "w") as zip_file:
50+
zip_file.writestr("MemOS-main/examples/test_example.py", "# Test example content")
51+
zip_file.writestr(
52+
"MemOS-main/examples/subfolder/another_example.py", "# Another example"
53+
)
54+
return zip_buffer.getvalue()
55+
56+
@patch("requests.get")
57+
@patch("os.makedirs")
58+
@patch("builtins.open", new_callable=mock_open)
59+
def test_download_examples_success(self, mock_file, mock_makedirs, mock_requests):
60+
"""Test successful examples download."""
61+
mock_response = MagicMock()
62+
mock_response.content = self.create_mock_zip_content()
63+
mock_requests.return_value = mock_response
64+
65+
result = download_examples("/test/dest")
66+
67+
assert result is True
68+
mock_requests.assert_called_once_with(
69+
"https://github.com/MemTensor/MemOS/archive/refs/heads/main.zip"
70+
)
71+
mock_response.raise_for_status.assert_called_once()
72+
73+
@patch("requests.get")
74+
def test_download_examples_error(self, mock_requests):
75+
"""Test download examples when request fails."""
76+
mock_requests.side_effect = requests.RequestException("Network error")
77+
78+
result = download_examples("/test/dest")
79+
80+
assert result is False
81+
82+
83+
class TestMainCLI:
84+
"""Test the main CLI function."""
85+
86+
@patch("memos.cli.download_examples")
87+
def test_main_download_examples(self, mock_download):
88+
"""Test main function with download_examples command."""
89+
mock_download.return_value = True
90+
91+
with patch("sys.argv", ["memos", "download_examples", "--dest", "/test/dest"]):
92+
with pytest.raises(SystemExit) as exc_info:
93+
main()
94+
assert exc_info.value.code == 0
95+
mock_download.assert_called_once_with("/test/dest")
96+
97+
@patch("memos.cli.export_openapi")
98+
def test_main_export_openapi(self, mock_export):
99+
"""Test main function with export_openapi command."""
100+
mock_export.return_value = True
101+
102+
with patch("sys.argv", ["memos", "export_openapi", "--output", "/test/openapi.json"]):
103+
with pytest.raises(SystemExit) as exc_info:
104+
main()
105+
assert exc_info.value.code == 0
106+
mock_export.assert_called_once_with("/test/openapi.json")

0 commit comments

Comments
 (0)