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 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
+ ```
@@ -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"]