oko-cli 0.1.0__tar.gz

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.
@@ -0,0 +1,85 @@
1
+ # Entornos virtuales
2
+ .env
3
+ .venv
4
+ venv/
5
+ env/
6
+ ENV/
7
+ env.bak/
8
+ venv.bak/
9
+
10
+ # Python cache y compilados
11
+ __pycache__/
12
+ *.py[cod]
13
+ *$py.class
14
+ *.so
15
+ .Python
16
+
17
+ # Eggs y distribución
18
+ *.egg
19
+ *.egg-info/
20
+ .eggs/
21
+ eggs/
22
+ develop-eggs/
23
+ oko.egg-info
24
+
25
+ # Build y distribución
26
+ build/
27
+ dist/
28
+ sdist/
29
+ wheels/
30
+ *.egg-info
31
+ *.egg
32
+
33
+ # Instalación
34
+ pip-log.txt
35
+ pip-delete-this-directory.txt
36
+
37
+ # Testing y coverage
38
+ .coverage
39
+ htmlcov/
40
+ .tox/
41
+ .nox/
42
+ .pytest_cache/
43
+ nosetests.xml
44
+ coverage.xml
45
+ *.cover
46
+
47
+ # Jupyter Notebook
48
+ .ipynb_checkpoints/
49
+
50
+ # IDE/Editores
51
+ .vscode/
52
+ .idea/
53
+ *.swp
54
+ *.swo
55
+ *~
56
+ .project
57
+ .pydevproject
58
+
59
+ # Sistema operativo
60
+ .DS_Store
61
+ .DS_Store?
62
+ ._*
63
+ .Spotlight-V100
64
+ .Trashes
65
+ ehthumbs.db
66
+ Thumbs.db
67
+
68
+ # Configuraciones y secretos
69
+ *.env
70
+ .env.local
71
+ .env.development.local
72
+ .env.test.local
73
+ .env.production.local
74
+ secrets.py
75
+ config_local.py
76
+ config.ini
77
+ settings_local.py
78
+ credentials.json
79
+ service_account.json
80
+
81
+ # Logs
82
+ *.log
83
+ logs/
84
+
85
+ # Archivos temporales
oko_cli-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Edward Ojeda
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
oko_cli-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,29 @@
1
+ Metadata-Version: 2.4
2
+ Name: oko-cli
3
+ Version: 0.1.0
4
+ Summary: Elegant CLI for API endpoint testing
5
+ Project-URL: Homepage, https://github.com/poll7872/oko
6
+ Project-URL: Repository, https://github.com/tuusuario/oko
7
+ Project-URL: Issues, https://github.com/poll7872/oko/issues
8
+ Author-email: Edward Ojeda <edward.ojeda.es@gmail.com>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: api,cli,endpoints,http,rest,testing
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.8
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Software Development :: Testing
23
+ Requires-Python: >=3.8
24
+ Requires-Dist: httpx>=0.25.0
25
+ Requires-Dist: rich>=13.0.0
26
+ Requires-Dist: typer>=0.9.0
27
+ Description-Content-Type: text/markdown
28
+
29
+ Proyecto OKO
@@ -0,0 +1 @@
1
+ Proyecto OKO
File without changes
@@ -0,0 +1,207 @@
1
+ from typing import List, Optional
2
+ import typer
3
+ from rich.prompt import Prompt, Confirm
4
+ from rich.panel import Panel
5
+ from datetime import datetime
6
+ from pathlib import Path
7
+
8
+ from .core.config import config, ConfigType, load_endpoints
9
+ from .core.endpoints import add_endpoint
10
+ from .core.runner import run_endpoint
11
+ from .ui import (
12
+ console,
13
+ print_logo,
14
+ print_success,
15
+ print_error,
16
+ print_warning,
17
+ print_info_panel,
18
+ get_table,
19
+ )
20
+
21
+ app = typer.Typer(
22
+ help="OKO CLI - API Testing made simple",
23
+ rich_markup_mode="rich",
24
+ no_args_is_help=True,
25
+ )
26
+ endpoint_app = typer.Typer(help="Manage API endpoints")
27
+ app.add_typer(endpoint_app, name="endpoint")
28
+
29
+
30
+ @app.command("init")
31
+ def init():
32
+ """Initialize OKO configuration."""
33
+ print_logo()
34
+ console.print("[bold primary]🚀 Welcome to OKO CLI![/bold primary]")
35
+ console.print("Let's set up your configuration.\n")
36
+
37
+ if config.find_config():
38
+ current_config = config.load_config()
39
+ warning_message = (
40
+ f"Configuration already exists at: [info]{config.config_dir}[/info]\n"
41
+ f"Type: [info]{current_config.get('type', 'unknown')}[/info]"
42
+ )
43
+ print_warning(warning_message, title="Configuration Found")
44
+
45
+ if not Confirm.ask("\nDo you want to reinitialize?"):
46
+ console.print("[dim]Initialization cancelled.[/dim]")
47
+ return
48
+
49
+ console.print("\n[bold primary]Where would you like to store endpoints?[/bold primary]")
50
+ options = [
51
+ ("project", "In this project (.oko/) - For team collaboration"),
52
+ ("global", "Global (~/.oko/) - For personal use"),
53
+ ("custom", "Custom location - Specify a path"),
54
+ ]
55
+
56
+ for i, (_, desc) in enumerate(options, 1):
57
+ console.print(f" [secondary]{i}[/secondary]. {desc}")
58
+
59
+ choice = Prompt.ask("\nSelect option (1-3)", choices=["1", "2", "3"], default="1")
60
+
61
+ config_type: ConfigType
62
+ config_dir: Path
63
+
64
+ if choice == "1":
65
+ config_type = "project"
66
+ config_dir = Path.cwd() / ".oko"
67
+ elif choice == "2":
68
+ config_type = "global"
69
+ config_dir = Path.home() / ".oko"
70
+ else:
71
+ config_type = "custom"
72
+ custom_path = Prompt.ask("\nEnter custom path")
73
+ config_dir = Path(custom_path).expanduser().resolve()
74
+
75
+ project_name = None
76
+ if config_type == "project":
77
+ project_name = Prompt.ask("\nProject name", default=Path.cwd().name)
78
+
79
+ summary = (
80
+ f" [bold]Type:[/bold] [info]{config_type}[/info]\n"
81
+ f" [bold]Location:[/bold] [info]{config_dir}[/info]\n"
82
+ )
83
+ if project_name:
84
+ summary += f" [bold]Project:[/bold] [info]{project_name}[/info]"
85
+
86
+ print_info_panel(summary, title="Summary")
87
+
88
+ if not Confirm.ask("\nCreate configuration?"):
89
+ console.print("[dim]Cancelled.[/dim]")
90
+ return
91
+
92
+ config_data = {
93
+ "created_at": datetime.now().isoformat(),
94
+ "project_name": project_name,
95
+ }
96
+ config.save_config(config_type, config_dir, config_data)
97
+
98
+ success_message = (
99
+ f"Configuration saved at: [info]{config.config_dir}[/info]\n\n"
100
+ "[bold]Next steps:[/bold]\n"
101
+ " • Add an endpoint: [secondary]oko endpoint add <alias> <url>[/secondary]\n"
102
+ " • Run an endpoint: [secondary]oko run <alias>[/secondary]"
103
+ )
104
+ print_success(success_message, title="✅ OKO initialized successfully!")
105
+
106
+
107
+ @app.command("info")
108
+ def info():
109
+ """Show current OKO configuration."""
110
+ if not config.find_config():
111
+ print_warning("No OKO configuration found. Run [secondary]oko init[/secondary] to get started.")
112
+ return
113
+
114
+ print_logo()
115
+ current_config = config.load_config()
116
+
117
+ config_details = (
118
+ f" [bold]Type:[/bold] [info]{current_config.get('type', 'unknown')}[/info]\n"
119
+ f" [bold]Location:[/bold] [info]{config.config_dir}[/info]\n"
120
+ f" [bold]Version:[/bold] [info]{current_config.get('version', '1.0')}[/info]\n"
121
+ )
122
+ if current_config.get("project_name"):
123
+ config_details += f" [bold]Project:[/bold] [info]{current_config.get('project_name')}[/info]\n"
124
+ if current_config.get("created_at"):
125
+ config_details += f" [bold]Created:[/bold] [info]{current_config.get('created_at')}[/info]"
126
+
127
+ print_info_panel(config_details, title="OKO Configuration")
128
+
129
+ endpoints_data = load_endpoints()
130
+ endpoints = endpoints_data.get("endpoints", {})
131
+ console.print(f"\n[bold primary]📊 Endpoints found:[/bold primary] [bold secondary]{len(endpoints)}[/bold secondary]")
132
+
133
+ if endpoints:
134
+ table = get_table()
135
+ table.add_column("Alias", style="blue", no_wrap=True)
136
+ table.add_column("Method", style="cyan")
137
+ table.add_column("URL", style="white")
138
+
139
+ for alias, endpoint in endpoints.items():
140
+ method = endpoint.get("method", "GET")
141
+ url = endpoint.get("url", "")
142
+ table.add_row(alias, method, url[:70] + ("..." if len(url) > 70 else ""))
143
+
144
+ console.print(table)
145
+
146
+
147
+ @endpoint_app.command("add")
148
+ def endpoint_add(
149
+ alias: str = typer.Argument(..., help="Alias to identify the endpoint"),
150
+ url: str = typer.Argument(..., help="Full URL of the endpoint"),
151
+ method: str = typer.Option(
152
+ "GET", "--method", "-M", help="HTTP method"
153
+ ),
154
+ ):
155
+ """Register a new endpoint."""
156
+ if not config.find_config():
157
+ print_warning("No OKO configuration found. Run [secondary]oko init[/secondary] first.")
158
+ raise typer.Exit(1)
159
+
160
+ method = method.upper()
161
+ SUPPORTED_METHODS = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]
162
+
163
+ if method not in SUPPORTED_METHODS:
164
+ print_error(f"Method '{method}' not supported. Choose from: [info]{', '.join(SUPPORTED_METHODS)}[/info]")
165
+ raise typer.Exit(1)
166
+
167
+ add_endpoint(alias, url, method)
168
+
169
+ message = (
170
+ f"Alias: [primary]{alias}[/primary]\n"
171
+ f"URL: [info]{method} {url}[/info]\n"
172
+ f"Saved in: [dim]{config.config_dir}[/dim]"
173
+ )
174
+ print_success(message, title=f"Endpoint '{alias}' saved!")
175
+
176
+
177
+ @app.command("run")
178
+ def run(
179
+ alias: str,
180
+ param: List[str] = typer.Option(
181
+ None, "--param", "-p", help="Query params (key=value). Use multiple times."
182
+ ),
183
+ header: List[str] = typer.Option(
184
+ None, "--header", "-H", help="HTTP headers (key=value). Use multiple times."
185
+ ),
186
+ body: Optional[str] = typer.Option(
187
+ None, "--body", "-B", help="JSON body or use '@file.json' to load from a file."
188
+ ),
189
+ ):
190
+ """
191
+ Run a saved endpoint.
192
+
193
+ Examples:
194
+ - [cyan]oko run my_api -p id=123[/cyan]
195
+ - [cyan]oko run create_user -B '{"name":"Test"}'[/cyan]
196
+ - [cyan]oko run upload -B @data.json[/cyan]
197
+ """
198
+ if not config.find_config():
199
+ print_warning("No OKO configuration found. Run [secondary]oko init[/secondary] first.")
200
+ raise typer.Exit(1)
201
+
202
+ with console.status(f"[bold green]Running [primary]'{alias}'[/primary]...[/bold green]", spinner="dots"):
203
+ run_endpoint(alias, console, param, header, body)
204
+
205
+
206
+ if __name__ == "__main__":
207
+ app()
File without changes
@@ -0,0 +1,132 @@
1
+ import json
2
+ from pathlib import Path
3
+ from typing import Optional, Literal
4
+
5
+ ConfigType = Literal["project", "global", "custom"]
6
+
7
+
8
+ class OkoConfig:
9
+ def __init__(self):
10
+ self.config_type: Optional[ConfigType] = None
11
+ self.config_dir: Optional[Path] = None
12
+ self.config_file: Optional[Path] = None
13
+ self.endpoints_file: Optional[Path] = None
14
+
15
+ def find_config(self) -> bool:
16
+ """
17
+ Buscar configuración jerárquicamente:
18
+ 1. Directorio actual o padres (.oko/)
19
+ 2. Global (~/.oko/)
20
+ 3. Si no existe, retornar False
21
+ """
22
+ # Buscar .oko/ en directorio actual o padres
23
+ current = Path.cwd()
24
+ for parent in [current] + list(current.parents):
25
+ oko_dir = parent / ".oko"
26
+ if oko_dir.exists() and (oko_dir / "config.json").exists():
27
+ self.config_type = "project"
28
+ self.config_dir = oko_dir
29
+ self.config_file = oko_dir / "config.json"
30
+ self.endpoints_file = oko_dir / "endpoints.json"
31
+ return True
32
+
33
+ # Buscar global
34
+ global_dir = Path.home() / ".oko"
35
+ if global_dir.exists() and (global_dir / "config.json").exists():
36
+ self.config_type = "global"
37
+ self.config_dir = global_dir
38
+ self.config_file = global_dir / "config.json"
39
+ self.endpoints_file = global_dir / "endpoints.json"
40
+ return True
41
+
42
+ return False
43
+
44
+ def load_config(self):
45
+ """Cargar configuración si existe"""
46
+ if self.config_file and self.config_file.exists():
47
+ try:
48
+ with open(self.config_file, "r", encoding="utf-8") as f:
49
+ content = f.read()
50
+ if not content.strip():
51
+ return {"version": "1.0", "type": self.config_type or "unknown"}
52
+ return json.loads(content)
53
+ except (json.JSONDecodeError, FileNotFoundError):
54
+ pass
55
+ return {"version": "1.0", "type": self.config_type or "unknown"}
56
+
57
+ def save_config(self, config_type: ConfigType, config_dir: Path, data: dict = None):
58
+ """Guardar nueva configuración"""
59
+ self.config_type = config_type
60
+ self.config_dir = config_dir
61
+ self.config_file = config_dir / "config.json"
62
+ self.endpoints_file = config_dir / "endpoints.json"
63
+
64
+ # Crear directorio
65
+ self.config_dir.mkdir(parents=True, exist_ok=True)
66
+
67
+ # Guardar config
68
+ config_data = {
69
+ "version": "1.0",
70
+ "type": config_type,
71
+ "created_at": data.get("created_at") if data else None,
72
+ "project_name": data.get("project_name") if data else None,
73
+ }
74
+
75
+ with open(self.config_file, "w", encoding="utf-8") as f:
76
+ json.dump(config_data, f, indent=2)
77
+
78
+ # Crear endpoints.json vacío si no existe
79
+ if not self.endpoints_file.exists():
80
+ with open(self.endpoints_file, "w", encoding="utf-8") as f:
81
+ json.dump({"endpoints": {}}, f, indent=2)
82
+
83
+ def get_endpoints_file(self) -> Path:
84
+ """Obtener archivo de endpoints (con fallback)"""
85
+ if self.endpoints_file and self.endpoints_file.exists():
86
+ return self.endpoints_file
87
+
88
+ # Si no hay config, usar global como fallback
89
+ if not self.find_config():
90
+ global_dir = Path.home() / ".oko"
91
+ global_dir.mkdir(parents=True, exist_ok=True)
92
+ return global_dir / "endpoints.json"
93
+
94
+ return self.endpoints_file
95
+
96
+
97
+ # Instancia global
98
+ config = OkoConfig()
99
+
100
+
101
+ def ensure_config():
102
+ """Versión simple para compatibilidad"""
103
+ return config.get_endpoints_file().parent.mkdir(parents=True, exist_ok=True)
104
+
105
+
106
+ def load_endpoints():
107
+ """Cargar endpoints desde la ubicación activa"""
108
+ endpoints_file = config.get_endpoints_file()
109
+
110
+ try:
111
+ if endpoints_file.exists():
112
+ with open(endpoints_file, "r", encoding="utf-8") as f:
113
+ content = f.read()
114
+ if not content.strip():
115
+ return {"endpoints": {}}
116
+ return json.loads(content)
117
+ except (json.JSONDecodeError, FileNotFoundError) as e:
118
+ print(f"[yellow]⚠ Error loading endpoints: {e}. Creating new.[/yellow]")
119
+
120
+ # Crear nuevo si hay error
121
+ with open(endpoints_file, "w", encoding="utf-8") as f:
122
+ json.dump({"endpoints": {}}, f, indent=2)
123
+ return {"endpoints": {}}
124
+
125
+
126
+ def save_endpoints(data: dict):
127
+ """Guardar endpoints en la ubicación activa"""
128
+ endpoints_file = config.get_endpoints_file()
129
+ endpoints_file.parent.mkdir(parents=True, exist_ok=True)
130
+
131
+ with open(endpoints_file, "w", encoding="utf-8") as f:
132
+ json.dump(data, f, indent=2)
@@ -0,0 +1,14 @@
1
+ from .config import load_endpoints, save_endpoints
2
+
3
+
4
+ def add_endpoint(alias: str, url: str, method: str = "GET"):
5
+ """Add a new endpoint using current config"""
6
+ data = load_endpoints()
7
+
8
+ endpoint_data = {
9
+ "url": url,
10
+ "method": method.upper(),
11
+ }
12
+
13
+ data["endpoints"][alias] = endpoint_data
14
+ save_endpoints(data)
@@ -0,0 +1,150 @@
1
+ import httpx
2
+ import json
3
+ from urllib.parse import parse_qs, urlencode, urlparse, urlunparse
4
+ from rich.console import Console
5
+ from rich.pretty import Pretty
6
+ from rich.table import Table
7
+ from rich.panel import Panel
8
+ from rich.box import ROUNDED
9
+
10
+ from .config import load_endpoints
11
+ from ..ui import print_error
12
+
13
+
14
+ def _make_key_value_table(data: dict) -> Table:
15
+ """Creates a styled table for key-value data."""
16
+ table = Table(box=ROUNDED, show_header=False, border_style="secondary")
17
+ table.add_column("Key", style="info", no_wrap=True)
18
+ table.add_column("Value", style="white")
19
+ for key, value in data.items():
20
+ table.add_row(str(key), str(value))
21
+ return table
22
+
23
+
24
+ def parse_params(param_list, console: Console):
25
+ """Convert ['a=1', 'b=2'] into {'a': '1', 'b': '2'}"""
26
+ params = {}
27
+ if not param_list:
28
+ return params
29
+ for item in param_list:
30
+ if "=" not in item:
31
+ console.print(f"[warning]Skipping invalid param:[/] {item}")
32
+ continue
33
+ key, value = item.split("=", 1)
34
+ params[key] = value
35
+ return params
36
+
37
+
38
+ def parse_headers(header_list, console: Console):
39
+ """Convert headers list to dict"""
40
+ headers = {}
41
+ if not header_list:
42
+ return headers
43
+ for item in header_list:
44
+ if "=" not in item:
45
+ console.print(f"[warning]Skipping invalid header:[/] {item}")
46
+ continue
47
+ key, value = item.split("=", 1)
48
+ headers[key] = value
49
+ return headers
50
+
51
+
52
+ def parse_body(body_data, console: Console):
53
+ """Parse JSON body from string or file"""
54
+ if not body_data:
55
+ return None
56
+
57
+ if body_data.startswith("@"):
58
+ filepath = body_data[1:]
59
+ try:
60
+ with open(filepath, "r") as f:
61
+ return json.load(f)
62
+ except Exception as e:
63
+ print_error(f"Error reading body file: {e}")
64
+ return None
65
+
66
+ try:
67
+ return json.loads(body_data)
68
+ except json.JSONDecodeError:
69
+ print_error(f"Invalid JSON body: {body_data}")
70
+ return None
71
+
72
+
73
+ def merge_query_params(url, extra_params):
74
+ """Merge base URL query params with user-provided params"""
75
+ parsed = urlparse(url)
76
+ base_params = {k: v[0] for k, v in parse_qs(parsed.query).items()}
77
+ final_params = {**base_params, **extra_params}
78
+ new_query = urlencode(final_params)
79
+
80
+ return urlunparse(
81
+ (
82
+ parsed.scheme,
83
+ parsed.netloc,
84
+ parsed.path,
85
+ parsed.params,
86
+ new_query,
87
+ parsed.fragment,
88
+ )
89
+ )
90
+
91
+
92
+ def run_endpoint(alias: str, console: Console, params=None, headers=None, body=None):
93
+ data = load_endpoints()
94
+ endpoints = data.get("endpoints", {})
95
+
96
+ if alias not in endpoints:
97
+ print_error(f"Endpoint '{alias}' not found.")
98
+ return
99
+
100
+ endpoint = endpoints[alias]
101
+ url = endpoint["url"]
102
+ method = endpoint["method"].upper()
103
+
104
+ # Parse inputs
105
+ extra_params = parse_params(params, console)
106
+ header_dict = parse_headers(headers, console)
107
+ body_data = parse_body(body, console)
108
+
109
+ # Merge URL params
110
+ final_url = merge_query_params(url, extra_params)
111
+
112
+ console.rule("[bold primary]Request[/bold primary]")
113
+ console.print(f"[bold]→ {method} {final_url}[/bold]")
114
+
115
+ if extra_params:
116
+ console.print(Panel(_make_key_value_table(extra_params), title="[info]Query Parameters[/info]", border_style="secondary"))
117
+
118
+ if header_dict:
119
+ console.print(Panel(_make_key_value_table(header_dict), title="[info]Headers[/info]", border_style="secondary"))
120
+
121
+ if body_data and method in ["POST", "PUT", "PATCH"]:
122
+ console.print(Panel(Pretty(body_data, expand_all=True), title="[info]Body[/info]", border_style="secondary"))
123
+
124
+ console.print()
125
+
126
+ try:
127
+ response = httpx.request(
128
+ method, final_url, headers=header_dict, json=body_data if body_data else None
129
+ )
130
+
131
+ status_style = "success" if response.is_success else "error"
132
+ console.rule(f"[bold {status_style}]Response[/bold {status_style}]")
133
+ console.print(f"\n[bold]Status:[/bold] [{status_style}]{response.status_code}[/]")
134
+
135
+ if response.headers:
136
+ console.print(Panel(_make_key_value_table(response.headers), title="[info]Headers[/info]", border_style="secondary"))
137
+
138
+ if response.content:
139
+ try:
140
+ json_data = response.json()
141
+ console.print(Panel(Pretty(json_data, expand_all=True), title="[info]Body[/info]", border_style="secondary"))
142
+ except Exception:
143
+ console.print(Panel(response.text[:2000], title="[warning]Response (Not JSON)[/warning]", border_style="warning"))
144
+ else:
145
+ console.print("\n[dim]No response body[/dim]")
146
+
147
+ except Exception as e:
148
+ print_error(f"Error connecting to endpoint: {e}")
149
+
150
+
@@ -0,0 +1,86 @@
1
+ from rich.console import Console
2
+ from rich.panel import Panel
3
+ from rich.theme import Theme
4
+ from rich.table import Table
5
+ from rich.box import ROUNDED
6
+
7
+ LOGO_CAT = """[bold blue]
8
+ /\\_/\\
9
+ ( o.o )
10
+ > ^ <
11
+ [/bold blue]"""
12
+
13
+ LOGO_OKO = """[bold bright_cyan]
14
+ ___ _ __ ___
15
+ / _ \| |/ / / _ \\
16
+ | | | | ' / | | | |
17
+ | |_| | . \\ | |_| |
18
+ \\___/|_|\\_\\ \\___/
19
+ [/bold bright_cyan]"""
20
+
21
+
22
+ # Custom theme for consistent styling
23
+ custom_theme = Theme(
24
+ {
25
+ "info": "cyan",
26
+ "warning": "yellow",
27
+ "error": "bold red",
28
+ "success": "bold green",
29
+ "primary": "blue",
30
+ "secondary": "bright_cyan",
31
+ }
32
+ )
33
+
34
+ # Global console object
35
+ console = Console(theme=custom_theme)
36
+
37
+
38
+ def print_logo():
39
+ """Prints the OKO logo."""
40
+ console.print(LOGO_CAT, justify="center")
41
+ console.print(LOGO_OKO, justify="center")
42
+
43
+
44
+ def print_success(message: str, title: str = "✅ Success"):
45
+ """Prints a success message in a panel."""
46
+ console.print(
47
+ Panel(
48
+ f"[success]{message}[/success]",
49
+ title=title,
50
+ border_style="success",
51
+ expand=False,
52
+ )
53
+ )
54
+
55
+
56
+ def print_error(message: str, title: str = "❌ Error"):
57
+ """Prints an error message in a panel."""
58
+ console.print(
59
+ Panel(
60
+ f"[error]{message}[/error]", title=title, border_style="error", expand=False
61
+ )
62
+ )
63
+
64
+
65
+ def print_warning(message: str, title: str = "⚠️ Warning"):
66
+ """Prints a warning message in a panel."""
67
+ console.print(
68
+ Panel(
69
+ f"[warning]{message}[/warning]",
70
+ title=title,
71
+ border_style="warning",
72
+ expand=False,
73
+ )
74
+ )
75
+
76
+
77
+ def print_info_panel(message: str, title: str = "📋 Info"):
78
+ """Prints an info message in a panel."""
79
+ console.print(Panel(message, title=title, border_style="info", expand=False))
80
+
81
+
82
+ def get_table() -> Table:
83
+ """Returns a nicely styled table."""
84
+ return Table(
85
+ show_header=True, header_style="bold bright_cyan", box=ROUNDED, padding=(0, 2)
86
+ )
@@ -0,0 +1,38 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "oko-cli"
7
+ version = "0.1.0"
8
+ description = "Elegant CLI for API endpoint testing"
9
+ readme = "README.md"
10
+ authors = [{ name = "Edward Ojeda", email = "edward.ojeda.es@gmail.com" }]
11
+ license = { text = "MIT" }
12
+ requires-python = ">=3.8"
13
+ keywords = ["api", "cli", "testing", "http", "rest", "endpoints"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "Topic :: Software Development :: Testing",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.8",
21
+ "Programming Language :: Python :: 3.9",
22
+ "Programming Language :: Python :: 3.10",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Programming Language :: Python :: 3.13",
26
+ ]
27
+ dependencies = ["httpx>=0.25.0", "typer>=0.9.0", "rich>=13.0.0"]
28
+
29
+ [project.scripts]
30
+ oko = "oko.cli:app"
31
+
32
+ [project.urls]
33
+ Homepage = "https://github.com/poll7872/oko"
34
+ Repository = "https://github.com/tuusuario/oko"
35
+ Issues = "https://github.com/poll7872/oko/issues"
36
+
37
+ [tool.hatch.build]
38
+ include = ["oko/**/*.py", "README.md", "LICENSE"]