Yuvacodes 0.1.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.
mycodes/__init__.py ADDED
@@ -0,0 +1,42 @@
1
+ """MyCodes: Personal cloud-based code library for Python and Jupyter Notebooks."""
2
+
3
+ from mycodes.config import configure, get_config
4
+ from mycodes.registry import get_code, get_info, list_codes, search, run
5
+ from mycodes.exceptions import (
6
+ MyCodesError,
7
+ CodeNotFoundError,
8
+ RepositoryNotFoundError,
9
+ ConfigurationError,
10
+ RemoteFetchError,
11
+ ExecutionDisabledError,
12
+ )
13
+ from mycodes.jupyter_extension import load_ipython_extension, unload_ipython_extension
14
+
15
+ __version__ = "0.1.0"
16
+
17
+ __all__ = [
18
+ "configure",
19
+ "get_config",
20
+ "get_code",
21
+ "get_info",
22
+ "list_codes",
23
+ "search",
24
+ "run",
25
+ "MyCodesError",
26
+ "CodeNotFoundError",
27
+ "RepositoryNotFoundError",
28
+ "ConfigurationError",
29
+ "RemoteFetchError",
30
+ "ExecutionDisabledError",
31
+ "load_ipython_extension",
32
+ "unload_ipython_extension",
33
+ ]
34
+
35
+ # Auto-register IPython extension if running inside an active IPython session
36
+ try:
37
+ from IPython import get_ipython
38
+ _ip = get_ipython()
39
+ if _ip is not None:
40
+ load_ipython_extension(_ip)
41
+ except Exception:
42
+ pass
mycodes/cli.py ADDED
@@ -0,0 +1,137 @@
1
+ """Command-line interface (CLI) for the MyCodes package."""
2
+
3
+ import argparse
4
+ import sys
5
+ from mycodes.config import configure, get_config
6
+ from mycodes.exceptions import MyCodesError
7
+ from mycodes.registry import get_code, get_info, list_codes, search, run
8
+
9
+
10
+ def build_parser() -> argparse.ArgumentParser:
11
+ """Construct command-line argument parser."""
12
+ parser = argparse.ArgumentParser(
13
+ prog="mycodes",
14
+ description="MyCodes - Your personal cloud-based code library CLI.",
15
+ )
16
+ subparsers = parser.add_subparsers(dest="command", help="Available commands")
17
+
18
+ # Command: list
19
+ list_p = subparsers.add_parser("list", help="List all available codes in the cloud library.")
20
+ list_p.add_argument("--refresh", "-r", action="store_true", help="Force refresh index from GitHub.")
21
+
22
+ # Command: get
23
+ get_p = subparsers.add_parser("get", help="Retrieve source code for a specific ID.")
24
+ get_p.add_argument("id", help="The numeric or string code ID (e.g., 101 or 001).")
25
+ get_p.add_argument("--cache", "-c", action="store_true", help="Cache the code locally.")
26
+ get_p.add_argument("--refresh", "-r", action="store_true", help="Force refresh from GitHub.")
27
+
28
+ # Command: info
29
+ info_p = subparsers.add_parser("info", help="Show metadata information for a code snippet.")
30
+ info_p.add_argument("id", help="The code ID.")
31
+ info_p.add_argument("--refresh", "-r", action="store_true", help="Force refresh from GitHub.")
32
+
33
+ # Command: search
34
+ search_p = subparsers.add_parser("search", help="Search codes by keyword, category, or language.")
35
+ search_p.add_argument("query", nargs="?", default=None, help="Search keyword.")
36
+ search_p.add_argument("--category", "-c", default=None, help="Filter by category.")
37
+ search_p.add_argument("--language", "-l", default=None, help="Filter by language.")
38
+
39
+ # Command: config
40
+ cfg_p = subparsers.add_parser("config", help="View or update MyCodes repository configuration.")
41
+ cfg_p.add_argument("--username", "-u", default=None, help="GitHub username.")
42
+ cfg_p.add_argument("--repository", "-r", default=None, help="Repository name.")
43
+ cfg_p.add_argument("--branch", "-b", default=None, help="Branch name.")
44
+
45
+ # Command: run
46
+ run_p = subparsers.add_parser("run", help="Execute remote code (requires explicit opt-in).")
47
+ run_p.add_argument("id", help="The code ID to execute.")
48
+ run_p.add_argument(
49
+ "--allow-execution",
50
+ action="store_true",
51
+ help="Explicitly acknowledge and allow execution of remote code.",
52
+ )
53
+
54
+ return parser
55
+
56
+
57
+ def main(args=None) -> int:
58
+ """CLI entry point function."""
59
+ parser = build_parser()
60
+ parsed = parser.parse_args(args)
61
+
62
+ if not parsed.command:
63
+ parser.print_help()
64
+ return 0
65
+
66
+ try:
67
+ if parsed.command == "list":
68
+ list_codes(return_data=False, refresh=parsed.refresh)
69
+ return 0
70
+
71
+ elif parsed.command == "get":
72
+ code = get_code(parsed.id, cache=parsed.cache, refresh=parsed.refresh)
73
+ print(code, end="")
74
+ if not code.endswith("\n"):
75
+ print()
76
+ return 0
77
+
78
+ elif parsed.command == "info":
79
+ info = get_info(parsed.id, refresh=parsed.refresh)
80
+ print(f"ID: {info.get('id')}")
81
+ print(f"Name: {info.get('name', 'N/A')}")
82
+ print(f"Description: {info.get('description', 'N/A')}")
83
+ print(f"Category: {info.get('category', 'N/A')}")
84
+ print(f"Language: {info.get('language', 'python')}")
85
+ if "tags" in info and info["tags"]:
86
+ tags_str = ", ".join(info["tags"]) if isinstance(info["tags"], list) else str(info["tags"])
87
+ print(f"Tags: {tags_str}")
88
+ print(f"File: {info.get('file', 'N/A')}")
89
+ return 0
90
+
91
+ elif parsed.command == "search":
92
+ search(
93
+ query=parsed.query,
94
+ category=parsed.category,
95
+ language=parsed.language,
96
+ return_data=False,
97
+ )
98
+ return 0
99
+
100
+ elif parsed.command == "config":
101
+ if parsed.username or parsed.repository or parsed.branch:
102
+ configure(
103
+ username=parsed.username,
104
+ repository=parsed.repository,
105
+ branch=parsed.branch,
106
+ persist=True,
107
+ )
108
+ print("Configuration saved to ~/.mycodes/config.json:")
109
+ current = get_config().to_dict()
110
+ print(f" Username: {current.get('username') or '(not set)'}")
111
+ print(f" Repository: {current.get('repository')}")
112
+ print(f" Branch: {current.get('branch')}")
113
+ return 0
114
+
115
+ elif parsed.command == "run":
116
+ if not parsed.allow_execution:
117
+ print(
118
+ "Error: Remote code execution is disabled by default for security.\n"
119
+ "Use --allow-execution to run this code.",
120
+ file=sys.stderr,
121
+ )
122
+ return 1
123
+ run(parsed.id, allow_execution=True)
124
+ return 0
125
+
126
+ except MyCodesError as e:
127
+ print(f"Error: {e}", file=sys.stderr)
128
+ return 1
129
+ except Exception as e:
130
+ print(f"Unexpected error: {e}", file=sys.stderr)
131
+ return 1
132
+
133
+ return 0
134
+
135
+
136
+ if __name__ == "__main__":
137
+ sys.exit(main())
mycodes/client.py ADDED
@@ -0,0 +1,174 @@
1
+ """HTTP client and caching layer for interacting with remote GitHub repository."""
2
+
3
+ import hashlib
4
+ import json
5
+ import time
6
+ import urllib.error
7
+ import urllib.request
8
+ from typing import Dict, Any, Optional
9
+ from mycodes.config import Config, get_config
10
+ from mycodes.exceptions import (
11
+ RepositoryNotFoundError,
12
+ RemoteFetchError,
13
+ )
14
+
15
+ USER_AGENT = "MyCodes-Python-Client/0.1.0"
16
+ INDEX_CACHE_TTL = 300 # 5 minutes default in-memory TTL
17
+
18
+
19
+ class GitHubClient:
20
+ """Handles communication with GitHub raw content and manages caching."""
21
+
22
+ def __init__(self, config: Optional[Config] = None):
23
+ self._config = config
24
+ self._index_cache: Optional[Dict[str, Any]] = None
25
+ self._index_fetched_at: float = 0.0
26
+
27
+ @property
28
+ def config(self) -> Config:
29
+ return self._config or get_config()
30
+
31
+ def fetch_url(self, url: str) -> str:
32
+ """Fetch raw text content from a given URL with timeout and custom headers."""
33
+ headers = {
34
+ "User-Agent": USER_AGENT,
35
+ "Accept": "text/plain, application/json, */*",
36
+ }
37
+ req = urllib.request.Request(url, headers=headers)
38
+ timeout = self.config.get_timeout()
39
+
40
+ try:
41
+ with urllib.request.urlopen(req, timeout=timeout) as response:
42
+ content_bytes = response.read()
43
+ return content_bytes.decode("utf-8")
44
+ except urllib.error.HTTPError as e:
45
+ if e.code == 404:
46
+ # 404 handled specifically by caller if needed
47
+ raise e
48
+ elif e.code == 403:
49
+ raise RemoteFetchError(
50
+ f"GitHub API/Raw rate limit exceeded or access forbidden (HTTP 403) at {url}."
51
+ ) from e
52
+ elif e.code >= 500:
53
+ raise RemoteFetchError(
54
+ f"GitHub server error (HTTP {e.code}) while requesting {url}."
55
+ ) from e
56
+ else:
57
+ raise RemoteFetchError(
58
+ f"HTTP error {e.code} ({e.reason}) while requesting {url}."
59
+ ) from e
60
+ except urllib.error.URLError as e:
61
+ raise RemoteFetchError(
62
+ f"Network connection failed: {e.reason} while requesting {url}."
63
+ ) from e
64
+ except TimeoutError as e:
65
+ raise RemoteFetchError(
66
+ f"Connection timed out after {timeout} seconds while requesting {url}."
67
+ ) from e
68
+ except Exception as e:
69
+ raise RemoteFetchError(
70
+ f"Unexpected error while fetching {url}: {str(e)}"
71
+ ) from e
72
+
73
+ def fetch_index(self, refresh: bool = False) -> Dict[str, Any]:
74
+ """
75
+ Fetch and parse index.json from the configured GitHub repository.
76
+ Uses in-memory cache unless expired or refresh=True.
77
+ """
78
+ now = time.time()
79
+ if (
80
+ not refresh
81
+ and self._index_cache is not None
82
+ and (now - self._index_fetched_at) < INDEX_CACHE_TTL
83
+ ):
84
+ return self._index_cache
85
+
86
+ url = self.config.get_raw_url("index.json")
87
+ try:
88
+ raw_data = self.fetch_url(url)
89
+ except urllib.error.HTTPError as e:
90
+ if e.code == 404:
91
+ raise RepositoryNotFoundError(
92
+ username=self.config.username,
93
+ repository=self.config.get_repository(),
94
+ branch=self.config.get_branch(),
95
+ ) from e
96
+ raise
97
+
98
+ try:
99
+ parsed = json.loads(raw_data)
100
+ except json.JSONDecodeError as e:
101
+ raise RemoteFetchError(
102
+ f"Failed to parse index.json from {url}: Invalid JSON structure ({str(e)})."
103
+ ) from e
104
+
105
+ if not isinstance(parsed, dict):
106
+ raise RemoteFetchError(
107
+ f"Invalid index.json schema from {url}: Expected root object (dict), got {type(parsed).__name__}."
108
+ )
109
+
110
+ self._index_cache = parsed
111
+ self._index_fetched_at = now
112
+ return parsed
113
+
114
+ def fetch_code(self, file_path: str, cache: bool = False, refresh: bool = False) -> str:
115
+ """
116
+ Fetch source code file from GitHub.
117
+ If cache=True, caches locally to avoid repeated downloads.
118
+ """
119
+ cache_file = None
120
+ if cache:
121
+ cache_dir = self.config.cache_dir
122
+ cache_dir.mkdir(parents=True, exist_ok=True)
123
+ # Create a deterministic filename based on url
124
+ cache_key = hashlib.sha256(file_path.encode("utf-8")).hexdigest()
125
+ cache_file = cache_dir / f"{cache_key}.py"
126
+
127
+ if not refresh and cache_file.exists():
128
+ try:
129
+ with open(cache_file, "r", encoding="utf-8") as f:
130
+ return f.read()
131
+ except Exception:
132
+ # On cache read failure, fall back to remote fetch
133
+ pass
134
+
135
+ url = self.config.get_raw_url(file_path)
136
+ try:
137
+ code = self.fetch_url(url)
138
+ except urllib.error.HTTPError as e:
139
+ if e.code == 404:
140
+ raise RemoteFetchError(
141
+ f"Code file '{file_path}' was not found at {url} (HTTP 404)."
142
+ ) from e
143
+ raise
144
+
145
+ if cache and cache_file is not None:
146
+ try:
147
+ with open(cache_file, "w", encoding="utf-8") as f:
148
+ f.write(code)
149
+ except Exception:
150
+ # Cache write error should not fail the user's operation
151
+ pass
152
+
153
+ return code
154
+
155
+ def clear_cache(self) -> None:
156
+ """Clear both in-memory index cache and disk cache."""
157
+ self._index_cache = None
158
+ self._index_fetched_at = 0.0
159
+ cache_dir = self.config.cache_dir
160
+ if cache_dir.exists():
161
+ for f in cache_dir.glob("*.py"):
162
+ try:
163
+ f.unlink()
164
+ except Exception:
165
+ pass
166
+
167
+
168
+ # Default singleton client
169
+ _DEFAULT_CLIENT = GitHubClient()
170
+
171
+
172
+ def get_client() -> GitHubClient:
173
+ """Return default singleton client."""
174
+ return _DEFAULT_CLIENT
mycodes/config.py ADDED
@@ -0,0 +1,192 @@
1
+ """Configuration management for the MyCodes library."""
2
+
3
+ import json
4
+ import os
5
+ from pathlib import Path
6
+ from typing import Optional, Dict, Any
7
+ from mycodes.exceptions import ConfigurationError
8
+
9
+ CONFIG_DIR = Path.home() / ".mycodes"
10
+ CONFIG_FILE = CONFIG_DIR / "config.json"
11
+
12
+ DEFAULT_REPOSITORY = "my-code-library"
13
+ DEFAULT_BRANCH = "main"
14
+ DEFAULT_TIMEOUT = 10.0
15
+
16
+
17
+ class Config:
18
+ """Holds configuration state for MyCodes."""
19
+
20
+ def __init__(
21
+ self,
22
+ username: Optional[str] = None,
23
+ repository: Optional[str] = None,
24
+ branch: Optional[str] = None,
25
+ cache_dir: Optional[Path] = None,
26
+ timeout: float = DEFAULT_TIMEOUT,
27
+ ):
28
+ self._username = username
29
+ self._repository = repository
30
+ self._branch = branch
31
+ self.cache_dir = cache_dir or (CONFIG_DIR / "cache")
32
+ self.timeout = timeout
33
+
34
+ @property
35
+ def username(self) -> str:
36
+ """Get the configured GitHub username or raise ConfigurationError."""
37
+ # 1. In-memory explicit setting
38
+ if self._username:
39
+ return self._username
40
+
41
+ # 2. Environment variable
42
+ env_user = os.environ.get("MYCODES_GITHUB_USER")
43
+ if env_user:
44
+ return env_user
45
+
46
+ # 3. Persistent config file
47
+ file_config = self._load_file_config()
48
+ if "username" in file_config and file_config["username"]:
49
+ return file_config["username"]
50
+
51
+ raise ConfigurationError(
52
+ "GitHub username is not configured. Please configure it via:\n"
53
+ " 1. configure(username='YOUR_GITHUB_USERNAME') in Python\n"
54
+ " 2. Setting the MYCODES_GITHUB_USER environment variable\n"
55
+ " 3. Running 'mycodes config --username YOUR_GITHUB_USERNAME' in your terminal."
56
+ )
57
+
58
+ @username.setter
59
+ def username(self, val: Optional[str]):
60
+ self._username = val
61
+
62
+ @property
63
+ def repository(self) -> str:
64
+ return self.get_repository()
65
+
66
+ @repository.setter
67
+ def repository(self, val: Optional[str]):
68
+ self._repository = val
69
+
70
+ @property
71
+ def branch(self) -> str:
72
+ return self.get_branch()
73
+
74
+ @branch.setter
75
+ def branch(self, val: Optional[str]):
76
+ self._branch = val
77
+
78
+ def get_repository(self) -> str:
79
+ """Get the configured repository name."""
80
+ if self._repository:
81
+ return self._repository
82
+ env_repo = os.environ.get("MYCODES_GITHUB_REPO")
83
+ if env_repo:
84
+ return env_repo
85
+ file_config = self._load_file_config()
86
+ return file_config.get("repository", DEFAULT_REPOSITORY)
87
+
88
+ def get_branch(self) -> str:
89
+ """Get the configured branch name."""
90
+ if self._branch:
91
+ return self._branch
92
+ env_branch = os.environ.get("MYCODES_GITHUB_BRANCH")
93
+ if env_branch:
94
+ return env_branch
95
+ file_config = self._load_file_config()
96
+ return file_config.get("branch", DEFAULT_BRANCH)
97
+
98
+ def get_timeout(self) -> float:
99
+ """Get network timeout in seconds."""
100
+ env_timeout = os.environ.get("MYCODES_TIMEOUT")
101
+ if env_timeout:
102
+ try:
103
+ return float(env_timeout)
104
+ except ValueError:
105
+ pass
106
+ return self.timeout
107
+
108
+ def get_raw_url(self, path: str) -> str:
109
+ """Construct raw GitHub URL for a given repository relative path."""
110
+ user = self.username
111
+ repo = self.get_repository()
112
+ branch = self.get_branch()
113
+ clean_path = path.lstrip("/")
114
+ return f"https://raw.githubusercontent.com/{user}/{repo}/{branch}/{clean_path}"
115
+
116
+ def to_dict(self) -> Dict[str, Any]:
117
+ """Return configuration as a dictionary."""
118
+ return {
119
+ "username": self._username or os.environ.get("MYCODES_GITHUB_USER") or self._load_file_config().get("username"),
120
+ "repository": self.get_repository(),
121
+ "branch": self.get_branch(),
122
+ "timeout": self.get_timeout(),
123
+ "cache_dir": str(self.cache_dir),
124
+ }
125
+
126
+ def _load_file_config(self) -> Dict[str, Any]:
127
+ """Load stored config from disk if exists."""
128
+ if CONFIG_FILE.exists():
129
+ try:
130
+ with open(CONFIG_FILE, "r", encoding="utf-8") as f:
131
+ return json.load(f)
132
+ except Exception:
133
+ return {}
134
+ return {}
135
+
136
+ def save_to_file(self) -> None:
137
+ """Save active config to ~/.mycodes/config.json."""
138
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
139
+ data = {
140
+ "username": self._username,
141
+ "repository": self.repository,
142
+ "branch": self.branch,
143
+ }
144
+ with open(CONFIG_FILE, "w", encoding="utf-8") as f:
145
+ json.dump(data, f, indent=2)
146
+
147
+
148
+ # Global singleton instance
149
+ _GLOBAL_CONFIG = Config()
150
+
151
+
152
+ def get_config() -> Config:
153
+ """Return the active global configuration."""
154
+ return _GLOBAL_CONFIG
155
+
156
+
157
+ def configure(
158
+ username: Optional[str] = None,
159
+ repository: Optional[str] = None,
160
+ branch: Optional[str] = None,
161
+ timeout: Optional[float] = None,
162
+ persist: bool = False,
163
+ ) -> Config:
164
+ """
165
+ Configure the MyCodes client.
166
+
167
+ Args:
168
+ username: GitHub username/organization owning the repository.
169
+ repository: Name of the repository (defaults to 'my-code-library').
170
+ branch: Git branch name (defaults to 'main').
171
+ timeout: Network timeout in seconds (defaults to 10.0).
172
+ persist: If True, writes settings to ~/.mycodes/config.json.
173
+ """
174
+ if username is not None:
175
+ _GLOBAL_CONFIG.username = username
176
+ if repository is not None:
177
+ _GLOBAL_CONFIG.repository = repository
178
+ if branch is not None:
179
+ _GLOBAL_CONFIG.branch = branch
180
+ if timeout is not None:
181
+ _GLOBAL_CONFIG.timeout = timeout
182
+
183
+ if persist:
184
+ _GLOBAL_CONFIG.save_to_file()
185
+
186
+ return _GLOBAL_CONFIG
187
+
188
+
189
+ def reset_config() -> None:
190
+ """Reset configuration to defaults (useful for testing)."""
191
+ global _GLOBAL_CONFIG
192
+ _GLOBAL_CONFIG = Config()
mycodes/exceptions.py ADDED
@@ -0,0 +1,54 @@
1
+ """Custom exceptions for the MyCodes library."""
2
+
3
+
4
+ class MyCodesError(Exception):
5
+ """Base exception for all MyCodes errors."""
6
+ pass
7
+
8
+
9
+ class CodeNotFoundError(MyCodesError):
10
+ """Raised when a requested code ID cannot be found in the repository."""
11
+
12
+ def __init__(self, code_id: str or int, message: str = None):
13
+ self.code_id = code_id
14
+ if message is None:
15
+ message = f"Code {code_id} was not found in the MyCodes repository."
16
+ super().__init__(message)
17
+
18
+
19
+ class RepositoryNotFoundError(MyCodesError):
20
+ """Raised when the remote GitHub repository, branch, or index cannot be accessed."""
21
+
22
+ def __init__(self, username: str, repository: str, branch: str = "main", message: str = None):
23
+ self.username = username
24
+ self.repository = repository
25
+ self.branch = branch
26
+ if message is None:
27
+ message = (
28
+ f"Repository '{username}/{repository}' on branch '{branch}' was not found on GitHub "
29
+ f"or index.json is missing."
30
+ )
31
+ super().__init__(message)
32
+
33
+
34
+ class ConfigurationError(MyCodesError):
35
+ """Raised when required configuration settings are missing or invalid."""
36
+ pass
37
+
38
+
39
+ class RemoteFetchError(MyCodesError):
40
+ """Raised when a network failure, timeout, or HTTP error occurs while fetching remote code."""
41
+ pass
42
+
43
+
44
+ class ExecutionDisabledError(MyCodesError):
45
+ """Raised when code execution is attempted without explicit authorization."""
46
+
47
+ def __init__(
48
+ self,
49
+ message: str = (
50
+ "Remote code execution is disabled by default for security. "
51
+ "Pass allow_execution=True to execute retrieved code."
52
+ ),
53
+ ):
54
+ super().__init__(message)