cogitan 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.
- cogitan-0.1.0/PKG-INFO +127 -0
- cogitan-0.1.0/README.md +112 -0
- cogitan-0.1.0/cogitan/__init__.py +36 -0
- cogitan-0.1.0/cogitan/cli.py +187 -0
- cogitan-0.1.0/cogitan/client.py +99 -0
- cogitan-0.1.0/cogitan/config.py +73 -0
- cogitan-0.1.0/cogitan/errors.py +31 -0
- cogitan-0.1.0/pyproject.toml +30 -0
cogitan-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cogitan
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CLI + SDK for the Cogitan Surrogates API — on-demand physics-simulation surrogate models.
|
|
5
|
+
Project-URL: Homepage, https://cogitan.ai
|
|
6
|
+
Project-URL: Documentation, https://docs.cogitan.ai
|
|
7
|
+
Author: Cogitan
|
|
8
|
+
Keywords: api,fno,neural-operator,physics,simulation,surrogate
|
|
9
|
+
Classifier: Intended Audience :: Science/Research
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Requires-Python: >=3.9
|
|
12
|
+
Requires-Dist: httpx>=0.27
|
|
13
|
+
Requires-Dist: typer>=0.12
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# cogitan
|
|
17
|
+
|
|
18
|
+
CLI + Python SDK for the **Cogitan Surrogates API** — call on-demand physics-simulation
|
|
19
|
+
surrogate models (heat, water, contaminant transport, materials, …) over HTTP. No models or
|
|
20
|
+
GPUs on your machine: you send inputs, the server runs the surrogate, you get results in ms.
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install cogitan
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
This gives you both the `cogitan` command and the `import cogitan` SDK.
|
|
29
|
+
|
|
30
|
+
## Set up your key (once)
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
cogitan login
|
|
34
|
+
# Paste your Cogitan API key: cog_sk_********
|
|
35
|
+
# ✓ Saved to ~/.cogitan/config.json
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Your key is stored in `~/.cogitan/config.json` (locked to your user) and used automatically
|
|
39
|
+
forever. Key resolution order, first match wins:
|
|
40
|
+
|
|
41
|
+
1. `--api-key` flag (one-off)
|
|
42
|
+
2. `COGITAN_API_KEY` env var (CI / containers)
|
|
43
|
+
3. `~/.cogitan/config.json` (the normal case)
|
|
44
|
+
|
|
45
|
+
To point at a non-default endpoint (e.g. local testing): `cogitan login --base-url http://localhost:8000`,
|
|
46
|
+
or set `COGITAN_BASE_URL`.
|
|
47
|
+
|
|
48
|
+
## Commands
|
|
49
|
+
|
|
50
|
+
| Command | What it does |
|
|
51
|
+
|---|---|
|
|
52
|
+
| `cogitan login` | Save your API key (prompts; persists forever) |
|
|
53
|
+
| `cogitan logout` | Delete the saved key |
|
|
54
|
+
| `cogitan whoami` | Show the active key (masked), endpoint, connection status |
|
|
55
|
+
| `cogitan models` | List the catalog of available models |
|
|
56
|
+
| `cogitan describe <model>` | Show a model's input schema (what fields to send) |
|
|
57
|
+
| `cogitan run <model>` | Run a prediction (see below) |
|
|
58
|
+
| `cogitan usage` | Current billing-period usage |
|
|
59
|
+
| `cogitan config` | Show config file location + settings |
|
|
60
|
+
| `cogitan version` | Print the version |
|
|
61
|
+
|
|
62
|
+
Add `--help` to any command for details.
|
|
63
|
+
|
|
64
|
+
## Running models on your own inputs
|
|
65
|
+
|
|
66
|
+
Three ways to provide inputs — mix and match:
|
|
67
|
+
|
|
68
|
+
**1. From a JSON file:**
|
|
69
|
+
```bash
|
|
70
|
+
cogitan run thermal --in my_case.json --out result.json
|
|
71
|
+
```
|
|
72
|
+
`my_case.json`:
|
|
73
|
+
```json
|
|
74
|
+
{
|
|
75
|
+
"grid_size": 64,
|
|
76
|
+
"conductivity": 50,
|
|
77
|
+
"sources": [{"x": 0.5, "y": 0.5, "amplitude": 30000, "width": 0.08}],
|
|
78
|
+
"boundary": {"left": {"type": "dirichlet", "value": 300}, "right": {"type": "dirichlet", "value": 350}}
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**2. Inline params** (repeatable; values are JSON-parsed, so numbers/lists/objects work):
|
|
83
|
+
```bash
|
|
84
|
+
cogitan run thermal -p conductivity=50 -p grid_size=64
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
**3. Piped from stdin:**
|
|
88
|
+
```bash
|
|
89
|
+
cat my_case.json | cogitan run thermal
|
|
90
|
+
echo '{"conductivity": 50}' | cogitan run thermal --out result.json
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Not sure what a model wants? `cogitan describe thermal` prints its input schema.
|
|
94
|
+
Without `--out`, the result prints to stdout (pipe it into `jq`, etc.).
|
|
95
|
+
|
|
96
|
+
## Python SDK
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
import cogitan
|
|
100
|
+
|
|
101
|
+
client = cogitan.Client() # key from config/env automatically
|
|
102
|
+
print(client.models()) # ["thermal", "groundwater", "contaminant", "mlip", ...]
|
|
103
|
+
|
|
104
|
+
# run a model
|
|
105
|
+
result = client.run("thermal", {
|
|
106
|
+
"conductivity": 50,
|
|
107
|
+
"sources": [{"x": 0.5, "y": 0.5, "amplitude": 30000, "width": 0.08}],
|
|
108
|
+
"boundary": {"left": {"type": "dirichlet", "value": 300}},
|
|
109
|
+
})
|
|
110
|
+
print(result["max_temperature"])
|
|
111
|
+
|
|
112
|
+
# namespaced sugar
|
|
113
|
+
result = client.thermal.predict(conductivity=50, sources=[...])
|
|
114
|
+
|
|
115
|
+
# one-liner with the default client
|
|
116
|
+
result = cogitan.run("thermal", {"conductivity": 50})
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Errors raise `cogitan.APIError` (with `.status_code`, `.code`, `.request_id`) or
|
|
120
|
+
`cogitan.NotConfigured` if no key is set.
|
|
121
|
+
|
|
122
|
+
## Local development
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
pip install -e . # from this directory
|
|
126
|
+
COGITAN_BASE_URL=http://localhost:8000 cogitan models
|
|
127
|
+
```
|
cogitan-0.1.0/README.md
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# cogitan
|
|
2
|
+
|
|
3
|
+
CLI + Python SDK for the **Cogitan Surrogates API** — call on-demand physics-simulation
|
|
4
|
+
surrogate models (heat, water, contaminant transport, materials, …) over HTTP. No models or
|
|
5
|
+
GPUs on your machine: you send inputs, the server runs the surrogate, you get results in ms.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install cogitan
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
This gives you both the `cogitan` command and the `import cogitan` SDK.
|
|
14
|
+
|
|
15
|
+
## Set up your key (once)
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
cogitan login
|
|
19
|
+
# Paste your Cogitan API key: cog_sk_********
|
|
20
|
+
# ✓ Saved to ~/.cogitan/config.json
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Your key is stored in `~/.cogitan/config.json` (locked to your user) and used automatically
|
|
24
|
+
forever. Key resolution order, first match wins:
|
|
25
|
+
|
|
26
|
+
1. `--api-key` flag (one-off)
|
|
27
|
+
2. `COGITAN_API_KEY` env var (CI / containers)
|
|
28
|
+
3. `~/.cogitan/config.json` (the normal case)
|
|
29
|
+
|
|
30
|
+
To point at a non-default endpoint (e.g. local testing): `cogitan login --base-url http://localhost:8000`,
|
|
31
|
+
or set `COGITAN_BASE_URL`.
|
|
32
|
+
|
|
33
|
+
## Commands
|
|
34
|
+
|
|
35
|
+
| Command | What it does |
|
|
36
|
+
|---|---|
|
|
37
|
+
| `cogitan login` | Save your API key (prompts; persists forever) |
|
|
38
|
+
| `cogitan logout` | Delete the saved key |
|
|
39
|
+
| `cogitan whoami` | Show the active key (masked), endpoint, connection status |
|
|
40
|
+
| `cogitan models` | List the catalog of available models |
|
|
41
|
+
| `cogitan describe <model>` | Show a model's input schema (what fields to send) |
|
|
42
|
+
| `cogitan run <model>` | Run a prediction (see below) |
|
|
43
|
+
| `cogitan usage` | Current billing-period usage |
|
|
44
|
+
| `cogitan config` | Show config file location + settings |
|
|
45
|
+
| `cogitan version` | Print the version |
|
|
46
|
+
|
|
47
|
+
Add `--help` to any command for details.
|
|
48
|
+
|
|
49
|
+
## Running models on your own inputs
|
|
50
|
+
|
|
51
|
+
Three ways to provide inputs — mix and match:
|
|
52
|
+
|
|
53
|
+
**1. From a JSON file:**
|
|
54
|
+
```bash
|
|
55
|
+
cogitan run thermal --in my_case.json --out result.json
|
|
56
|
+
```
|
|
57
|
+
`my_case.json`:
|
|
58
|
+
```json
|
|
59
|
+
{
|
|
60
|
+
"grid_size": 64,
|
|
61
|
+
"conductivity": 50,
|
|
62
|
+
"sources": [{"x": 0.5, "y": 0.5, "amplitude": 30000, "width": 0.08}],
|
|
63
|
+
"boundary": {"left": {"type": "dirichlet", "value": 300}, "right": {"type": "dirichlet", "value": 350}}
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**2. Inline params** (repeatable; values are JSON-parsed, so numbers/lists/objects work):
|
|
68
|
+
```bash
|
|
69
|
+
cogitan run thermal -p conductivity=50 -p grid_size=64
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**3. Piped from stdin:**
|
|
73
|
+
```bash
|
|
74
|
+
cat my_case.json | cogitan run thermal
|
|
75
|
+
echo '{"conductivity": 50}' | cogitan run thermal --out result.json
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Not sure what a model wants? `cogitan describe thermal` prints its input schema.
|
|
79
|
+
Without `--out`, the result prints to stdout (pipe it into `jq`, etc.).
|
|
80
|
+
|
|
81
|
+
## Python SDK
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
import cogitan
|
|
85
|
+
|
|
86
|
+
client = cogitan.Client() # key from config/env automatically
|
|
87
|
+
print(client.models()) # ["thermal", "groundwater", "contaminant", "mlip", ...]
|
|
88
|
+
|
|
89
|
+
# run a model
|
|
90
|
+
result = client.run("thermal", {
|
|
91
|
+
"conductivity": 50,
|
|
92
|
+
"sources": [{"x": 0.5, "y": 0.5, "amplitude": 30000, "width": 0.08}],
|
|
93
|
+
"boundary": {"left": {"type": "dirichlet", "value": 300}},
|
|
94
|
+
})
|
|
95
|
+
print(result["max_temperature"])
|
|
96
|
+
|
|
97
|
+
# namespaced sugar
|
|
98
|
+
result = client.thermal.predict(conductivity=50, sources=[...])
|
|
99
|
+
|
|
100
|
+
# one-liner with the default client
|
|
101
|
+
result = cogitan.run("thermal", {"conductivity": 50})
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Errors raise `cogitan.APIError` (with `.status_code`, `.code`, `.request_id`) or
|
|
105
|
+
`cogitan.NotConfigured` if no key is set.
|
|
106
|
+
|
|
107
|
+
## Local development
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
pip install -e . # from this directory
|
|
111
|
+
COGITAN_BASE_URL=http://localhost:8000 cogitan models
|
|
112
|
+
```
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Cogitan Surrogates API — Python SDK + CLI.
|
|
2
|
+
|
|
3
|
+
import cogitan
|
|
4
|
+
client = cogitan.Client() # reads key from ~/.cogitan/config.json or env
|
|
5
|
+
print(client.models())
|
|
6
|
+
result = client.run("thermal", {...})
|
|
7
|
+
# or:
|
|
8
|
+
result = cogitan.run("thermal", {...})
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from .client import Client
|
|
12
|
+
from .errors import CogitanError, NotConfigured, APIError
|
|
13
|
+
|
|
14
|
+
__version__ = "0.1.0"
|
|
15
|
+
|
|
16
|
+
_default_client = None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _default() -> "Client":
|
|
20
|
+
global _default_client
|
|
21
|
+
if _default_client is None:
|
|
22
|
+
_default_client = Client()
|
|
23
|
+
return _default_client
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def run(model: str, inputs: dict | None = None, **kwargs):
|
|
27
|
+
"""Convenience: cogitan.run("thermal", {...}) or cogitan.run("thermal", conductivity=50)."""
|
|
28
|
+
return _default().run(model, inputs if inputs is not None else kwargs)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def models():
|
|
32
|
+
"""List the catalog of available models."""
|
|
33
|
+
return _default().models()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
__all__ = ["Client", "run", "models", "CogitanError", "NotConfigured", "APIError", "__version__"]
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""Cogitan CLI — `cogitan <command>`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import List, Optional
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
|
|
12
|
+
from . import __version__
|
|
13
|
+
from . import config as _config
|
|
14
|
+
from .client import Client
|
|
15
|
+
from .errors import APIError, CogitanError, NotConfigured
|
|
16
|
+
|
|
17
|
+
app = typer.Typer(
|
|
18
|
+
add_completion=False,
|
|
19
|
+
no_args_is_help=True,
|
|
20
|
+
help="Cogitan Surrogates API — run physics-simulation surrogate models on demand.",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _client() -> Client:
|
|
25
|
+
try:
|
|
26
|
+
return Client()
|
|
27
|
+
except NotConfigured as e:
|
|
28
|
+
typer.secho(str(e), fg=typer.colors.RED)
|
|
29
|
+
raise typer.Exit(1)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _mask(key: str) -> str:
|
|
33
|
+
return (key[:12] + "…") if key and len(key) > 12 else (key or "")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# --------------------------------------------------------------------------- auth
|
|
37
|
+
|
|
38
|
+
@app.command()
|
|
39
|
+
def login(
|
|
40
|
+
api_key: Optional[str] = typer.Option(None, "--api-key", help="API key (omit to be prompted)"),
|
|
41
|
+
base_url: Optional[str] = typer.Option(None, "--base-url", help="override the API base URL"),
|
|
42
|
+
):
|
|
43
|
+
"""Save your API key. Persists in ~/.cogitan/config.json — set it once, use it forever."""
|
|
44
|
+
if not api_key:
|
|
45
|
+
api_key = typer.prompt("Paste your Cogitan API key", hide_input=True)
|
|
46
|
+
api_key = api_key.strip()
|
|
47
|
+
|
|
48
|
+
# best-effort validation
|
|
49
|
+
try:
|
|
50
|
+
Client(api_key=api_key, base_url=base_url).health()
|
|
51
|
+
except APIError as e:
|
|
52
|
+
if e.status_code in (401, 403):
|
|
53
|
+
typer.secho("That key was rejected by the server.", fg=typer.colors.RED)
|
|
54
|
+
raise typer.Exit(1)
|
|
55
|
+
typer.secho(f"Note: couldn't verify the key ({e}). Saving anyway.", fg=typer.colors.YELLOW)
|
|
56
|
+
except CogitanError as e:
|
|
57
|
+
typer.secho(f"Note: couldn't reach the server ({e}). Saving anyway.", fg=typer.colors.YELLOW)
|
|
58
|
+
|
|
59
|
+
_config.set_api_key(api_key, base_url)
|
|
60
|
+
typer.secho(f"✓ Saved to {_config.CONFIG_FILE}", fg=typer.colors.GREEN)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@app.command()
|
|
64
|
+
def logout():
|
|
65
|
+
"""Delete the saved API key."""
|
|
66
|
+
if _config.clear():
|
|
67
|
+
typer.secho("✓ Logged out.", fg=typer.colors.GREEN)
|
|
68
|
+
else:
|
|
69
|
+
typer.echo("No saved key.")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@app.command()
|
|
73
|
+
def whoami():
|
|
74
|
+
"""Show the active key (masked), endpoint, and connection status."""
|
|
75
|
+
key = _config.resolve_api_key()
|
|
76
|
+
if not key:
|
|
77
|
+
typer.secho("Not logged in. Run `cogitan login`.", fg=typer.colors.RED)
|
|
78
|
+
raise typer.Exit(1)
|
|
79
|
+
typer.echo(f"key: {_mask(key)}")
|
|
80
|
+
typer.echo(f"endpoint: {_config.resolve_base_url()}")
|
|
81
|
+
try:
|
|
82
|
+
_client().health()
|
|
83
|
+
typer.secho("status: connected ✓", fg=typer.colors.GREEN)
|
|
84
|
+
except CogitanError as e:
|
|
85
|
+
typer.secho(f"status: {e}", fg=typer.colors.YELLOW)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# --------------------------------------------------------------------------- catalog
|
|
89
|
+
|
|
90
|
+
@app.command()
|
|
91
|
+
def models(json_out: bool = typer.Option(False, "--json", help="raw JSON output")):
|
|
92
|
+
"""List the catalog of available models."""
|
|
93
|
+
ms = _client().models()
|
|
94
|
+
if json_out:
|
|
95
|
+
typer.echo(json.dumps(ms, indent=2))
|
|
96
|
+
return
|
|
97
|
+
if not ms:
|
|
98
|
+
typer.echo("(no models)")
|
|
99
|
+
return
|
|
100
|
+
for m in ms:
|
|
101
|
+
avail = "available" if m.get("available") else "unavailable"
|
|
102
|
+
typer.echo(f" {str(m.get('name')):<16} {avail:<12} {m.get('description') or ''}")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@app.command()
|
|
106
|
+
def describe(model: str = typer.Argument(..., help="model name, e.g. thermal")):
|
|
107
|
+
"""Show a model's input schema (what fields to send)."""
|
|
108
|
+
m = _client().describe(model)
|
|
109
|
+
typer.echo(json.dumps(m.get("input_schema") or m, indent=2))
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@app.command()
|
|
113
|
+
def run(
|
|
114
|
+
model: str = typer.Argument(..., help="model name, e.g. thermal"),
|
|
115
|
+
in_: Optional[str] = typer.Option(None, "--in", "-i", help="JSON input file ('-' for stdin)"),
|
|
116
|
+
out: Optional[Path] = typer.Option(None, "--out", "-o", help="write JSON result to this file"),
|
|
117
|
+
param: List[str] = typer.Option(None, "--param", "-p", help="inline input key=value (repeatable)"),
|
|
118
|
+
):
|
|
119
|
+
"""Run a model. Inputs come from --in (file/stdin), piped stdin, and/or --param overrides."""
|
|
120
|
+
inputs: dict = {}
|
|
121
|
+
|
|
122
|
+
if in_ is not None:
|
|
123
|
+
text = sys.stdin.read() if in_ == "-" else Path(in_).read_text()
|
|
124
|
+
inputs = json.loads(text) if text.strip() else {}
|
|
125
|
+
elif not sys.stdin.isatty():
|
|
126
|
+
piped = sys.stdin.read().strip()
|
|
127
|
+
if piped:
|
|
128
|
+
inputs = json.loads(piped)
|
|
129
|
+
|
|
130
|
+
for p in (param or []):
|
|
131
|
+
if "=" not in p:
|
|
132
|
+
typer.secho(f"bad --param '{p}' (expected key=value)", fg=typer.colors.RED)
|
|
133
|
+
raise typer.Exit(1)
|
|
134
|
+
k, v = p.split("=", 1)
|
|
135
|
+
try:
|
|
136
|
+
inputs[k] = json.loads(v) # coerce numbers / bools / lists / json objects
|
|
137
|
+
except Exception:
|
|
138
|
+
inputs[k] = v # fall back to a plain string
|
|
139
|
+
|
|
140
|
+
try:
|
|
141
|
+
result = _client().run(model, inputs)
|
|
142
|
+
except APIError as e:
|
|
143
|
+
typer.secho(str(e), fg=typer.colors.RED)
|
|
144
|
+
raise typer.Exit(1)
|
|
145
|
+
|
|
146
|
+
text = json.dumps(result, indent=2)
|
|
147
|
+
if out is not None:
|
|
148
|
+
Path(out).write_text(text)
|
|
149
|
+
typer.secho(f"✓ wrote {out}", fg=typer.colors.GREEN)
|
|
150
|
+
else:
|
|
151
|
+
typer.echo(text)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
# --------------------------------------------------------------------------- account / meta
|
|
155
|
+
|
|
156
|
+
@app.command()
|
|
157
|
+
def usage():
|
|
158
|
+
"""Show your current billing-period usage."""
|
|
159
|
+
try:
|
|
160
|
+
typer.echo(json.dumps(_client().usage(), indent=2))
|
|
161
|
+
except APIError as e:
|
|
162
|
+
if e.status_code == 404:
|
|
163
|
+
typer.secho("Usage endpoint not available yet — see the web dashboard.",
|
|
164
|
+
fg=typer.colors.YELLOW)
|
|
165
|
+
else:
|
|
166
|
+
typer.secho(str(e), fg=typer.colors.RED)
|
|
167
|
+
raise typer.Exit(1)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@app.command()
|
|
171
|
+
def config():
|
|
172
|
+
"""Show the config file location and current settings."""
|
|
173
|
+
typer.echo(f"config file: {_config.CONFIG_FILE}")
|
|
174
|
+
data = _config.load()
|
|
175
|
+
if "api_key" in data:
|
|
176
|
+
data = {**data, "api_key": _mask(data["api_key"])}
|
|
177
|
+
typer.echo(json.dumps(data, indent=2))
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@app.command()
|
|
181
|
+
def version():
|
|
182
|
+
"""Print the cogitan version."""
|
|
183
|
+
typer.echo(__version__)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
if __name__ == "__main__":
|
|
187
|
+
app()
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""The Cogitan SDK — a thin HTTP client over the Surrogates API gateway."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from . import config as _config
|
|
10
|
+
from .errors import APIError, NotConfigured
|
|
11
|
+
|
|
12
|
+
_USER_AGENT = "cogitan-python/0.1.0"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class _ModelProxy:
|
|
16
|
+
"""Sugar so `client.thermal.predict(conductivity=50)` == `client.run("thermal", {...})`."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, client: "Client", model: str):
|
|
19
|
+
self._client = client
|
|
20
|
+
self._model = model
|
|
21
|
+
|
|
22
|
+
def predict(self, **inputs) -> dict:
|
|
23
|
+
return self._client.run(self._model, inputs)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Client:
|
|
27
|
+
"""Talks to the Cogitan gateway with your API key.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
api_key: overrides the resolved key (else env / config file).
|
|
31
|
+
base_url: overrides the API base URL (else env / config / default).
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, api_key: str | None = None, base_url: str | None = None, timeout: float = 60.0):
|
|
35
|
+
key = _config.resolve_api_key(api_key)
|
|
36
|
+
if not key:
|
|
37
|
+
raise NotConfigured()
|
|
38
|
+
self.api_key = key
|
|
39
|
+
self.base_url = _config.resolve_base_url(base_url).rstrip("/")
|
|
40
|
+
self._http = httpx.Client(
|
|
41
|
+
timeout=timeout,
|
|
42
|
+
headers={"Authorization": f"Bearer {key}", "User-Agent": _USER_AGENT},
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# -------------------------------------------------------------- lifecycle
|
|
46
|
+
def close(self) -> None:
|
|
47
|
+
self._http.close()
|
|
48
|
+
|
|
49
|
+
def __enter__(self) -> "Client":
|
|
50
|
+
return self
|
|
51
|
+
|
|
52
|
+
def __exit__(self, *exc) -> None:
|
|
53
|
+
self.close()
|
|
54
|
+
|
|
55
|
+
# -------------------------------------------------------------- transport
|
|
56
|
+
def _request(self, method: str, path: str, json: Any = None) -> Any:
|
|
57
|
+
try:
|
|
58
|
+
r = self._http.request(method, self.base_url + path, json=json)
|
|
59
|
+
except httpx.HTTPError as e:
|
|
60
|
+
raise APIError(0, "connection_error", f"could not reach {self.base_url}: {e}")
|
|
61
|
+
if r.status_code >= 400:
|
|
62
|
+
code = message = request_id = None
|
|
63
|
+
try:
|
|
64
|
+
err = r.json().get("error", {})
|
|
65
|
+
code, message, request_id = err.get("code"), err.get("message"), err.get("request_id")
|
|
66
|
+
except Exception:
|
|
67
|
+
message = (r.text or "")[:300]
|
|
68
|
+
raise APIError(r.status_code, code, message, request_id)
|
|
69
|
+
return r.json() if r.content else {}
|
|
70
|
+
|
|
71
|
+
# -------------------------------------------------------------- endpoints
|
|
72
|
+
def health(self) -> dict:
|
|
73
|
+
return self._request("GET", "/health")
|
|
74
|
+
|
|
75
|
+
def models(self) -> list[dict]:
|
|
76
|
+
"""List the catalog: [{name, available, description, input_schema}, ...]."""
|
|
77
|
+
return self._request("GET", "/v1/models").get("models", [])
|
|
78
|
+
|
|
79
|
+
def describe(self, model: str) -> dict:
|
|
80
|
+
"""Full metadata (incl. input_schema) for one model."""
|
|
81
|
+
for m in self.models():
|
|
82
|
+
if m.get("name") == model:
|
|
83
|
+
return m
|
|
84
|
+
raise APIError(404, "not_found", f"model '{model}' not found in the catalog")
|
|
85
|
+
|
|
86
|
+
def run(self, model: str, inputs: dict | None = None) -> dict:
|
|
87
|
+
"""Run a prediction. Returns the model's response dict."""
|
|
88
|
+
return self._request("POST", f"/v1/models/{model}/predict", json=inputs or {})
|
|
89
|
+
|
|
90
|
+
def usage(self) -> dict:
|
|
91
|
+
"""Current billing-period usage."""
|
|
92
|
+
return self._request("GET", "/v1/usage")
|
|
93
|
+
|
|
94
|
+
# -------------------------------------------------------------- sugar
|
|
95
|
+
def __getattr__(self, name: str) -> _ModelProxy:
|
|
96
|
+
# only reached for unknown attributes; enables client.<model>.predict(...)
|
|
97
|
+
if name.startswith("_"):
|
|
98
|
+
raise AttributeError(name)
|
|
99
|
+
return _ModelProxy(self, name)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Config + credential resolution.
|
|
2
|
+
|
|
3
|
+
API-key resolution order (first match wins):
|
|
4
|
+
1. explicit api_key argument / --api-key flag
|
|
5
|
+
2. COGITAN_API_KEY environment variable (CI, containers, temporary)
|
|
6
|
+
3. ~/.cogitan/config.json (written by `cogitan login` — the normal case)
|
|
7
|
+
|
|
8
|
+
So a user runs `cogitan login` once and the key persists forever, while CI can override via env.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
import stat
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
CONFIG_DIR = Path.home() / ".cogitan"
|
|
19
|
+
CONFIG_FILE = CONFIG_DIR / "config.json"
|
|
20
|
+
DEFAULT_BASE_URL = "https://api.cogitan.ai"
|
|
21
|
+
|
|
22
|
+
ENV_API_KEY = "COGITAN_API_KEY"
|
|
23
|
+
ENV_BASE_URL = "COGITAN_BASE_URL"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def load() -> dict:
|
|
27
|
+
if CONFIG_FILE.exists():
|
|
28
|
+
try:
|
|
29
|
+
return json.loads(CONFIG_FILE.read_text())
|
|
30
|
+
except Exception:
|
|
31
|
+
return {}
|
|
32
|
+
return {}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def save(data: dict) -> None:
|
|
36
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
37
|
+
CONFIG_FILE.write_text(json.dumps(data, indent=2))
|
|
38
|
+
try:
|
|
39
|
+
# 0600 — owner read/write only (best-effort; no-op on some Windows filesystems)
|
|
40
|
+
CONFIG_FILE.chmod(stat.S_IRUSR | stat.S_IWUSR)
|
|
41
|
+
except Exception:
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def set_api_key(api_key: str, base_url: str | None = None) -> None:
|
|
46
|
+
data = load()
|
|
47
|
+
data["api_key"] = api_key
|
|
48
|
+
if base_url:
|
|
49
|
+
data["base_url"] = base_url
|
|
50
|
+
save(data)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def clear() -> bool:
|
|
54
|
+
if CONFIG_FILE.exists():
|
|
55
|
+
CONFIG_FILE.unlink()
|
|
56
|
+
return True
|
|
57
|
+
return False
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def resolve_api_key(explicit: str | None = None) -> str | None:
|
|
61
|
+
if explicit:
|
|
62
|
+
return explicit
|
|
63
|
+
if os.environ.get(ENV_API_KEY):
|
|
64
|
+
return os.environ[ENV_API_KEY]
|
|
65
|
+
return load().get("api_key")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def resolve_base_url(explicit: str | None = None) -> str:
|
|
69
|
+
if explicit:
|
|
70
|
+
return explicit
|
|
71
|
+
if os.environ.get(ENV_BASE_URL):
|
|
72
|
+
return os.environ[ENV_BASE_URL]
|
|
73
|
+
return load().get("base_url") or DEFAULT_BASE_URL
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Exceptions for the Cogitan client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class CogitanError(Exception):
|
|
7
|
+
"""Base class for all Cogitan client errors."""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class NotConfigured(CogitanError):
|
|
11
|
+
"""No API key available."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, message: str | None = None):
|
|
14
|
+
super().__init__(
|
|
15
|
+
message
|
|
16
|
+
or "No API key found. Run `cogitan login` (or set the COGITAN_API_KEY env var)."
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class APIError(CogitanError):
|
|
21
|
+
"""The API returned an error (or could not be reached)."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, status_code: int, code: str | None = None,
|
|
24
|
+
message: str | None = None, request_id: str | None = None):
|
|
25
|
+
self.status_code = status_code
|
|
26
|
+
self.code = code
|
|
27
|
+
self.request_id = request_id
|
|
28
|
+
text = f"[{status_code}] {code or 'error'}: {message or ''}".rstrip()
|
|
29
|
+
if request_id:
|
|
30
|
+
text += f" (request_id={request_id})"
|
|
31
|
+
super().__init__(text)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "cogitan"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "CLI + SDK for the Cogitan Surrogates API — on-demand physics-simulation surrogate models."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
authors = [{ name = "Cogitan" }]
|
|
12
|
+
keywords = ["surrogate", "simulation", "physics", "neural-operator", "fno", "api"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Intended Audience :: Science/Research",
|
|
16
|
+
]
|
|
17
|
+
dependencies = [
|
|
18
|
+
"typer>=0.12",
|
|
19
|
+
"httpx>=0.27",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[project.scripts]
|
|
23
|
+
cogitan = "cogitan.cli:app"
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Homepage = "https://cogitan.ai"
|
|
27
|
+
Documentation = "https://docs.cogitan.ai"
|
|
28
|
+
|
|
29
|
+
[tool.hatch.build.targets.wheel]
|
|
30
|
+
packages = ["cogitan"]
|