powerbase-cli 0.1.5__tar.gz → 0.2.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.
- {powerbase_cli-0.1.5 → powerbase_cli-0.2.0}/PKG-INFO +5 -3
- {powerbase_cli-0.1.5 → powerbase_cli-0.2.0}/README.md +4 -2
- {powerbase_cli-0.1.5 → powerbase_cli-0.2.0}/pyproject.toml +1 -4
- {powerbase_cli-0.1.5 → powerbase_cli-0.2.0}/src/powerbase_cli/__init__.py +1 -1
- {powerbase_cli-0.1.5 → powerbase_cli-0.2.0}/src/powerbase_cli/commands/parser.py +0 -16
- {powerbase_cli-0.1.5 → powerbase_cli-0.2.0}/src/powerbase_cli/commands/shared.py +3 -15
- {powerbase_cli-0.1.5 → powerbase_cli-0.2.0}/src/powerbase_cli/config.py +4 -54
- {powerbase_cli-0.1.5 → powerbase_cli-0.2.0}/src/powerbase_cli/session.py +1 -20
- {powerbase_cli-0.1.5 → powerbase_cli-0.2.0}/src/powerbase_cli/transport.py +1 -15
- {powerbase_cli-0.1.5 → powerbase_cli-0.2.0}/src/powerbase_cli.egg-info/PKG-INFO +5 -3
- {powerbase_cli-0.1.5 → powerbase_cli-0.2.0}/src/powerbase_cli.egg-info/SOURCES.txt +0 -1
- {powerbase_cli-0.1.5 → powerbase_cli-0.2.0}/tests/test_cli_commands.py +5 -26
- {powerbase_cli-0.1.5 → powerbase_cli-0.2.0}/tests/test_cli_help.py +4 -4
- {powerbase_cli-0.1.5 → powerbase_cli-0.2.0}/tests/test_config.py +1 -15
- powerbase_cli-0.2.0/tests/test_session.py +97 -0
- {powerbase_cli-0.1.5 → powerbase_cli-0.2.0}/tests/test_transport.py +1 -66
- powerbase_cli-0.1.5/src/powerbase_cli/certs/powerbase-test-ca.pem +0 -21
- powerbase_cli-0.1.5/tests/test_session.py +0 -194
- {powerbase_cli-0.1.5 → powerbase_cli-0.2.0}/setup.cfg +0 -0
- {powerbase_cli-0.1.5 → powerbase_cli-0.2.0}/src/powerbase_cli/__main__.py +0 -0
- {powerbase_cli-0.1.5 → powerbase_cli-0.2.0}/src/powerbase_cli/api.py +0 -0
- {powerbase_cli-0.1.5 → powerbase_cli-0.2.0}/src/powerbase_cli/cli.py +0 -0
- {powerbase_cli-0.1.5 → powerbase_cli-0.2.0}/src/powerbase_cli/commands/__init__.py +0 -0
- {powerbase_cli-0.1.5 → powerbase_cli-0.2.0}/src/powerbase_cli/commands/agent.py +0 -0
- {powerbase_cli-0.1.5 → powerbase_cli-0.2.0}/src/powerbase_cli/commands/auth.py +0 -0
- {powerbase_cli-0.1.5 → powerbase_cli-0.2.0}/src/powerbase_cli/commands/branch.py +0 -0
- {powerbase_cli-0.1.5 → powerbase_cli-0.2.0}/src/powerbase_cli/commands/config_cmd.py +0 -0
- {powerbase_cli-0.1.5 → powerbase_cli-0.2.0}/src/powerbase_cli/commands/context.py +0 -0
- {powerbase_cli-0.1.5 → powerbase_cli-0.2.0}/src/powerbase_cli/commands/database.py +0 -0
- {powerbase_cli-0.1.5 → powerbase_cli-0.2.0}/src/powerbase_cli/commands/instance.py +0 -0
- {powerbase_cli-0.1.5 → powerbase_cli-0.2.0}/src/powerbase_cli/commands/org.py +0 -0
- {powerbase_cli-0.1.5 → powerbase_cli-0.2.0}/src/powerbase_cli/commands/publish.py +0 -0
- {powerbase_cli-0.1.5 → powerbase_cli-0.2.0}/src/powerbase_cli/commands/sandbox.py +0 -0
- {powerbase_cli-0.1.5 → powerbase_cli-0.2.0}/src/powerbase_cli/commands/sql.py +0 -0
- {powerbase_cli-0.1.5 → powerbase_cli-0.2.0}/src/powerbase_cli.egg-info/dependency_links.txt +0 -0
- {powerbase_cli-0.1.5 → powerbase_cli-0.2.0}/src/powerbase_cli.egg-info/entry_points.txt +0 -0
- {powerbase_cli-0.1.5 → powerbase_cli-0.2.0}/src/powerbase_cli.egg-info/top_level.txt +0 -0
- {powerbase_cli-0.1.5 → powerbase_cli-0.2.0}/tests/test_api.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: powerbase-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: CLI for operating Powerbase console workflows
|
|
5
5
|
Author: Powerbase
|
|
6
6
|
Requires-Python: >=3.11
|
|
@@ -41,10 +41,12 @@ Database model:
|
|
|
41
41
|
Install from PyPI:
|
|
42
42
|
|
|
43
43
|
```bash
|
|
44
|
-
python -m pip install powerbase-cli
|
|
44
|
+
python -m pip install --upgrade powerbase-cli==0.2.0
|
|
45
45
|
powerbase --help
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
+
Version `0.2.x` is the production-pinned release line and always targets `https://console.appbuild.chat`.
|
|
49
|
+
|
|
48
50
|
From the repository root during development:
|
|
49
51
|
|
|
50
52
|
```bash
|
|
@@ -121,7 +123,7 @@ powerbase auth token-set \
|
|
|
121
123
|
--expires-at 1760000000
|
|
122
124
|
```
|
|
123
125
|
|
|
124
|
-
|
|
126
|
+
The CLI is pinned to the production Powerbase console deployment and no longer exposes endpoint override flags.
|
|
125
127
|
|
|
126
128
|
### Check Auth State
|
|
127
129
|
|
|
@@ -33,10 +33,12 @@ Database model:
|
|
|
33
33
|
Install from PyPI:
|
|
34
34
|
|
|
35
35
|
```bash
|
|
36
|
-
python -m pip install powerbase-cli
|
|
36
|
+
python -m pip install --upgrade powerbase-cli==0.2.0
|
|
37
37
|
powerbase --help
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
+
Version `0.2.x` is the production-pinned release line and always targets `https://console.appbuild.chat`.
|
|
41
|
+
|
|
40
42
|
From the repository root during development:
|
|
41
43
|
|
|
42
44
|
```bash
|
|
@@ -113,7 +115,7 @@ powerbase auth token-set \
|
|
|
113
115
|
--expires-at 1760000000
|
|
114
116
|
```
|
|
115
117
|
|
|
116
|
-
|
|
118
|
+
The CLI is pinned to the production Powerbase console deployment and no longer exposes endpoint override flags.
|
|
117
119
|
|
|
118
120
|
### Check Auth State
|
|
119
121
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "powerbase-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
description = "CLI for operating Powerbase console workflows"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -17,8 +17,5 @@ powerbase = "powerbase_cli.cli:main"
|
|
|
17
17
|
[tool.setuptools]
|
|
18
18
|
package-dir = { "" = "src" }
|
|
19
19
|
|
|
20
|
-
[tool.setuptools.package-data]
|
|
21
|
-
powerbase_cli = ["certs/*.pem"]
|
|
22
|
-
|
|
23
20
|
[tool.setuptools.packages.find]
|
|
24
21
|
where = ["src"]
|
|
@@ -19,10 +19,6 @@ from .sql import register_sql_commands
|
|
|
19
19
|
|
|
20
20
|
GLOBAL_OPTION_ARITY = {
|
|
21
21
|
"--config-dir": 1,
|
|
22
|
-
"--base-url": 1,
|
|
23
|
-
"--anon-key": 1,
|
|
24
|
-
"--ca-cert": 1,
|
|
25
|
-
"--insecure": 0,
|
|
26
22
|
"--json": 0,
|
|
27
23
|
}
|
|
28
24
|
|
|
@@ -74,18 +70,6 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
74
70
|
),
|
|
75
71
|
)
|
|
76
72
|
parser.add_argument("--config-dir", help="Override the config directory. Defaults to ~/.config/powerbase.")
|
|
77
|
-
parser.add_argument("--base-url", help="Console base URL. Overrides the built-in default console endpoint.")
|
|
78
|
-
parser.add_argument("--anon-key", help="Supabase anon key used for functions and auth requests. Overrides the built-in default.")
|
|
79
|
-
parser.add_argument(
|
|
80
|
-
"--ca-cert",
|
|
81
|
-
dest="ca_cert_file",
|
|
82
|
-
help="Path to a CA certificate PEM file to trust for HTTPS requests.",
|
|
83
|
-
)
|
|
84
|
-
parser.add_argument(
|
|
85
|
-
"--insecure",
|
|
86
|
-
action="store_true",
|
|
87
|
-
help="Disable TLS certificate verification for HTTPS requests. Use only for testing with self-signed certs.",
|
|
88
|
-
)
|
|
89
73
|
parser.add_argument("--json", action="store_true", help="Print JSON output.")
|
|
90
74
|
|
|
91
75
|
subparsers = parser.add_subparsers(dest="command")
|
|
@@ -8,7 +8,6 @@ from typing import Any
|
|
|
8
8
|
|
|
9
9
|
from ..api import PowerbaseApi
|
|
10
10
|
from ..config import (
|
|
11
|
-
BUNDLED_CA_CERT_SENTINEL,
|
|
12
11
|
DEFAULT_ANON_KEY,
|
|
13
12
|
DEFAULT_BASE_URL,
|
|
14
13
|
AppConfig,
|
|
@@ -16,8 +15,6 @@ from ..config import (
|
|
|
16
15
|
AuthState,
|
|
17
16
|
ConfigStore,
|
|
18
17
|
ContextState,
|
|
19
|
-
load_bundled_ca_cert,
|
|
20
|
-
merge_config_with_env,
|
|
21
18
|
merge_context_with_env,
|
|
22
19
|
)
|
|
23
20
|
from ..session import SessionManager
|
|
@@ -33,18 +30,11 @@ def build_store(args: argparse.Namespace) -> ConfigStore:
|
|
|
33
30
|
|
|
34
31
|
|
|
35
32
|
def resolve_config(args: argparse.Namespace, store: ConfigStore) -> AppConfig:
|
|
36
|
-
config =
|
|
37
|
-
saved_auth = store.load_auth()
|
|
38
|
-
explicit_ca_cert_file = getattr(args, "ca_cert_file", None) or config.ca_cert_file
|
|
33
|
+
config = store.load_config()
|
|
39
34
|
return AppConfig(
|
|
40
|
-
base_url=
|
|
41
|
-
anon_key=
|
|
35
|
+
base_url=DEFAULT_BASE_URL,
|
|
36
|
+
anon_key=DEFAULT_ANON_KEY,
|
|
42
37
|
output="json" if args.json else config.output,
|
|
43
|
-
tls_insecure=bool(getattr(args, "insecure", False) or config.tls_insecure),
|
|
44
|
-
# Keep explicit CA settings first. The bundled test CA is only the final fallback for the
|
|
45
|
-
# current self-signed test environment and should be removed once the deployment uses a
|
|
46
|
-
# publicly trusted certificate.
|
|
47
|
-
ca_cert_file=explicit_ca_cert_file or (BUNDLED_CA_CERT_SENTINEL if load_bundled_ca_cert() else None),
|
|
48
38
|
)
|
|
49
39
|
|
|
50
40
|
|
|
@@ -60,8 +50,6 @@ def build_api(args: argparse.Namespace) -> tuple[ConfigStore, AppConfig, Context
|
|
|
60
50
|
store,
|
|
61
51
|
config.base_url,
|
|
62
52
|
config.anon_key,
|
|
63
|
-
tls_insecure=config.tls_insecure,
|
|
64
|
-
ca_cert_file=config.ca_cert_file,
|
|
65
53
|
)
|
|
66
54
|
api = PowerbaseApi(PowerbaseTransport(config, session_manager))
|
|
67
55
|
return store, config, context, session_manager, api
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import importlib.resources
|
|
4
3
|
import json
|
|
5
4
|
import os
|
|
6
5
|
from dataclasses import asdict, dataclass
|
|
@@ -12,13 +11,8 @@ try:
|
|
|
12
11
|
except ModuleNotFoundError: # pragma: no cover
|
|
13
12
|
tomllib = None # type: ignore[assignment]
|
|
14
13
|
|
|
15
|
-
DEFAULT_BASE_URL = "https://console.
|
|
14
|
+
DEFAULT_BASE_URL = "https://console.appbuild.chat"
|
|
16
15
|
DEFAULT_ANON_KEY = "reallyreallyreallyreallyverysafe"
|
|
17
|
-
# This bundled CA is only for the self-signed test deployment so `pip install powerbase-cli`
|
|
18
|
-
# can work out of the box. Remove it after the deployed console switches to a publicly trusted
|
|
19
|
-
# TLS certificate, then delete the bundled PEM file and its package-data entry as well.
|
|
20
|
-
BUNDLED_CA_CERT_SENTINEL = "<bundled test CA>"
|
|
21
|
-
BUNDLED_CA_CERT_RESOURCE = "certs/powerbase-test-ca.pem"
|
|
22
16
|
|
|
23
17
|
|
|
24
18
|
def default_config_dir() -> Path:
|
|
@@ -33,8 +27,6 @@ class AppConfig:
|
|
|
33
27
|
base_url: str | None = DEFAULT_BASE_URL
|
|
34
28
|
anon_key: str | None = DEFAULT_ANON_KEY
|
|
35
29
|
output: str = "text"
|
|
36
|
-
tls_insecure: bool = False
|
|
37
|
-
ca_cert_file: str | None = None
|
|
38
30
|
|
|
39
31
|
|
|
40
32
|
@dataclass
|
|
@@ -69,26 +61,6 @@ def _as_path(path: str | Path | None) -> Path:
|
|
|
69
61
|
return Path(path).expanduser()
|
|
70
62
|
|
|
71
63
|
|
|
72
|
-
def env_flag(name: str) -> bool:
|
|
73
|
-
value = os.environ.get(name)
|
|
74
|
-
if value is None:
|
|
75
|
-
return False
|
|
76
|
-
return value.strip().lower() in {"1", "true", "yes", "on"}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
def load_bundled_ca_cert() -> str | None:
|
|
80
|
-
# The PEM contents are loaded at runtime so test builds can trust the packaged self-signed CA
|
|
81
|
-
# without asking end users to pass `--ca-cert`. Once the server uses a trusted certificate,
|
|
82
|
-
# this helper and its callers can be removed.
|
|
83
|
-
try:
|
|
84
|
-
resource = importlib.resources.files("powerbase_cli").joinpath(BUNDLED_CA_CERT_RESOURCE)
|
|
85
|
-
if not resource.is_file():
|
|
86
|
-
return None
|
|
87
|
-
return resource.read_text(encoding="utf-8")
|
|
88
|
-
except (FileNotFoundError, ModuleNotFoundError):
|
|
89
|
-
return None
|
|
90
|
-
|
|
91
|
-
|
|
92
64
|
class ConfigStore:
|
|
93
65
|
def __init__(self, base_dir: str | Path | None = None) -> None:
|
|
94
66
|
self.base_dir = _as_path(base_dir)
|
|
@@ -101,31 +73,19 @@ class ConfigStore:
|
|
|
101
73
|
|
|
102
74
|
def load_config(self) -> AppConfig:
|
|
103
75
|
if not self.config_path.exists():
|
|
104
|
-
return AppConfig(
|
|
76
|
+
return AppConfig()
|
|
105
77
|
if tomllib is None:
|
|
106
78
|
raise RuntimeError("tomllib is required to read config.toml")
|
|
107
79
|
data = tomllib.loads(self.config_path.read_text(encoding="utf-8"))
|
|
108
80
|
return AppConfig(
|
|
109
|
-
base_url=data.get("base_url"),
|
|
110
|
-
anon_key=data.get("anon_key"),
|
|
111
81
|
output=data.get("output", "text"),
|
|
112
|
-
tls_insecure=bool(data.get("tls_insecure", False)),
|
|
113
|
-
ca_cert_file=data.get("ca_cert_file"),
|
|
114
82
|
)
|
|
115
83
|
|
|
116
84
|
def save_config(self, config: AppConfig) -> None:
|
|
117
85
|
self.ensure_base_dir()
|
|
118
86
|
lines = []
|
|
119
|
-
if config.base_url and config.base_url != DEFAULT_BASE_URL:
|
|
120
|
-
lines.append(f'base_url = "{config.base_url}"')
|
|
121
|
-
if config.anon_key and config.anon_key != DEFAULT_ANON_KEY:
|
|
122
|
-
lines.append(f'anon_key = "{config.anon_key}"')
|
|
123
87
|
if config.output:
|
|
124
88
|
lines.append(f'output = "{config.output}"')
|
|
125
|
-
if config.tls_insecure:
|
|
126
|
-
lines.append("tls_insecure = true")
|
|
127
|
-
if config.ca_cert_file:
|
|
128
|
-
lines.append(f'ca_cert_file = "{config.ca_cert_file}"')
|
|
129
89
|
self.config_path.write_text("\n".join(lines) + ("\n" if lines else ""), encoding="utf-8")
|
|
130
90
|
|
|
131
91
|
def load_auth(self) -> AuthState | None:
|
|
@@ -210,8 +170,8 @@ def env_auth_state() -> AuthState | None:
|
|
|
210
170
|
expires_at = int(expires_at_raw) if expires_at_raw and expires_at_raw.isdigit() else None
|
|
211
171
|
return AuthState(
|
|
212
172
|
source="env",
|
|
213
|
-
base_url=
|
|
214
|
-
anon_key=
|
|
173
|
+
base_url=DEFAULT_BASE_URL,
|
|
174
|
+
anon_key=DEFAULT_ANON_KEY,
|
|
215
175
|
session=AuthSession(
|
|
216
176
|
access_token=access_token,
|
|
217
177
|
refresh_token=refresh_token,
|
|
@@ -220,16 +180,6 @@ def env_auth_state() -> AuthState | None:
|
|
|
220
180
|
)
|
|
221
181
|
|
|
222
182
|
|
|
223
|
-
def merge_config_with_env(config: AppConfig) -> AppConfig:
|
|
224
|
-
return AppConfig(
|
|
225
|
-
base_url=os.environ.get("POWERBASE_BASE_URL") or config.base_url,
|
|
226
|
-
anon_key=os.environ.get("POWERBASE_ANON_KEY") or config.anon_key,
|
|
227
|
-
output=os.environ.get("POWERBASE_OUTPUT") or config.output,
|
|
228
|
-
tls_insecure=env_flag("POWERBASE_TLS_INSECURE") or config.tls_insecure,
|
|
229
|
-
ca_cert_file=os.environ.get("POWERBASE_CA_CERT_FILE") or config.ca_cert_file,
|
|
230
|
-
)
|
|
231
|
-
|
|
232
|
-
|
|
233
183
|
def merge_context_with_env(context: ContextState) -> ContextState:
|
|
234
184
|
return ContextState(
|
|
235
185
|
instance_id=os.environ.get("POWERBASE_INSTANCE_ID") or context.instance_id,
|
|
@@ -11,7 +11,7 @@ from typing import Iterator
|
|
|
11
11
|
from urllib import request
|
|
12
12
|
from urllib.error import HTTPError, URLError
|
|
13
13
|
|
|
14
|
-
from .config import
|
|
14
|
+
from .config import AuthState, ConfigStore, env_auth_state
|
|
15
15
|
|
|
16
16
|
try:
|
|
17
17
|
import fcntl
|
|
@@ -29,15 +29,10 @@ class SessionManager:
|
|
|
29
29
|
store: ConfigStore,
|
|
30
30
|
base_url: str | None,
|
|
31
31
|
anon_key: str | None,
|
|
32
|
-
*,
|
|
33
|
-
tls_insecure: bool = False,
|
|
34
|
-
ca_cert_file: str | None = None,
|
|
35
32
|
) -> None:
|
|
36
33
|
self.store = store
|
|
37
34
|
self.base_url = base_url
|
|
38
35
|
self.anon_key = anon_key
|
|
39
|
-
self.tls_insecure = tls_insecure
|
|
40
|
-
self.ca_cert_file = ca_cert_file
|
|
41
36
|
|
|
42
37
|
def _login_guidance(self) -> str:
|
|
43
38
|
return (
|
|
@@ -47,20 +42,6 @@ class SessionManager:
|
|
|
47
42
|
)
|
|
48
43
|
|
|
49
44
|
def _urlopen(self, req: request.Request):
|
|
50
|
-
if self.tls_insecure:
|
|
51
|
-
context = ssl.create_default_context()
|
|
52
|
-
context.check_hostname = False
|
|
53
|
-
context.verify_mode = ssl.CERT_NONE
|
|
54
|
-
return request.urlopen(req, context=context)
|
|
55
|
-
if self.ca_cert_file:
|
|
56
|
-
if self.ca_cert_file == BUNDLED_CA_CERT_SENTINEL:
|
|
57
|
-
bundled_ca_cert = load_bundled_ca_cert()
|
|
58
|
-
if not bundled_ca_cert:
|
|
59
|
-
raise SessionError("Bundled test CA certificate is unavailable.")
|
|
60
|
-
context = ssl.create_default_context(cadata=bundled_ca_cert)
|
|
61
|
-
else:
|
|
62
|
-
context = ssl.create_default_context(cafile=self.ca_cert_file)
|
|
63
|
-
return request.urlopen(req, context=context)
|
|
64
45
|
return request.urlopen(req)
|
|
65
46
|
|
|
66
47
|
def get_auth_state(self) -> AuthState | None:
|
|
@@ -7,7 +7,7 @@ from typing import Any
|
|
|
7
7
|
from urllib import request
|
|
8
8
|
from urllib.error import HTTPError, URLError
|
|
9
9
|
|
|
10
|
-
from .config import
|
|
10
|
+
from .config import AppConfig
|
|
11
11
|
from .session import SessionError, SessionManager
|
|
12
12
|
|
|
13
13
|
|
|
@@ -35,20 +35,6 @@ class PowerbaseTransport:
|
|
|
35
35
|
)
|
|
36
36
|
|
|
37
37
|
def _urlopen(self, req: request.Request):
|
|
38
|
-
if self.config.tls_insecure:
|
|
39
|
-
context = ssl.create_default_context()
|
|
40
|
-
context.check_hostname = False
|
|
41
|
-
context.verify_mode = ssl.CERT_NONE
|
|
42
|
-
return request.urlopen(req, context=context)
|
|
43
|
-
if self.config.ca_cert_file:
|
|
44
|
-
if self.config.ca_cert_file == BUNDLED_CA_CERT_SENTINEL:
|
|
45
|
-
bundled_ca_cert = load_bundled_ca_cert()
|
|
46
|
-
if not bundled_ca_cert:
|
|
47
|
-
raise ApiError("Bundled test CA certificate is unavailable.")
|
|
48
|
-
context = ssl.create_default_context(cadata=bundled_ca_cert)
|
|
49
|
-
else:
|
|
50
|
-
context = ssl.create_default_context(cafile=self.config.ca_cert_file)
|
|
51
|
-
return request.urlopen(req, context=context)
|
|
52
38
|
return request.urlopen(req)
|
|
53
39
|
|
|
54
40
|
def _build_headers(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: powerbase-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: CLI for operating Powerbase console workflows
|
|
5
5
|
Author: Powerbase
|
|
6
6
|
Requires-Python: >=3.11
|
|
@@ -41,10 +41,12 @@ Database model:
|
|
|
41
41
|
Install from PyPI:
|
|
42
42
|
|
|
43
43
|
```bash
|
|
44
|
-
python -m pip install powerbase-cli
|
|
44
|
+
python -m pip install --upgrade powerbase-cli==0.2.0
|
|
45
45
|
powerbase --help
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
+
Version `0.2.x` is the production-pinned release line and always targets `https://console.appbuild.chat`.
|
|
49
|
+
|
|
48
50
|
From the repository root during development:
|
|
49
51
|
|
|
50
52
|
```bash
|
|
@@ -121,7 +123,7 @@ powerbase auth token-set \
|
|
|
121
123
|
--expires-at 1760000000
|
|
122
124
|
```
|
|
123
125
|
|
|
124
|
-
|
|
126
|
+
The CLI is pinned to the production Powerbase console deployment and no longer exposes endpoint override flags.
|
|
125
127
|
|
|
126
128
|
### Check Auth State
|
|
127
129
|
|
|
@@ -12,7 +12,6 @@ src/powerbase_cli.egg-info/SOURCES.txt
|
|
|
12
12
|
src/powerbase_cli.egg-info/dependency_links.txt
|
|
13
13
|
src/powerbase_cli.egg-info/entry_points.txt
|
|
14
14
|
src/powerbase_cli.egg-info/top_level.txt
|
|
15
|
-
src/powerbase_cli/certs/powerbase-test-ca.pem
|
|
16
15
|
src/powerbase_cli/commands/__init__.py
|
|
17
16
|
src/powerbase_cli/commands/agent.py
|
|
18
17
|
src/powerbase_cli/commands/auth.py
|
|
@@ -16,7 +16,6 @@ from powerbase_cli.commands.auth import handle_auth_wait as auth_handle_auth_wai
|
|
|
16
16
|
from powerbase_cli.commands.agent import handle_agent_chat as agent_handle_agent_chat
|
|
17
17
|
from powerbase_cli.commands.shared import resolve_config as shared_resolve_config
|
|
18
18
|
from powerbase_cli.config import (
|
|
19
|
-
BUNDLED_CA_CERT_SENTINEL,
|
|
20
19
|
AppConfig,
|
|
21
20
|
AuthSession,
|
|
22
21
|
AuthState,
|
|
@@ -82,20 +81,12 @@ class CliCommandTests(unittest.TestCase):
|
|
|
82
81
|
def test_resolve_config_falls_back_to_builtin_service_defaults(self) -> None:
|
|
83
82
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
84
83
|
store = ConfigStore(Path(temp_dir))
|
|
85
|
-
args = Namespace(
|
|
86
|
-
config_dir=temp_dir,
|
|
87
|
-
base_url=None,
|
|
88
|
-
anon_key=None,
|
|
89
|
-
json=False,
|
|
90
|
-
insecure=False,
|
|
91
|
-
ca_cert_file=None,
|
|
92
|
-
)
|
|
84
|
+
args = Namespace(config_dir=temp_dir, json=False)
|
|
93
85
|
resolved = shared_resolve_config(args, store)
|
|
94
86
|
self.assertEqual(resolved.base_url, DEFAULT_BASE_URL)
|
|
95
87
|
self.assertEqual(resolved.anon_key, DEFAULT_ANON_KEY)
|
|
96
|
-
self.assertEqual(resolved.ca_cert_file, BUNDLED_CA_CERT_SENTINEL)
|
|
97
88
|
|
|
98
|
-
def
|
|
89
|
+
def test_resolve_config_ignores_saved_auth_endpoint_overrides(self) -> None:
|
|
99
90
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
100
91
|
store = ConfigStore(Path(temp_dir))
|
|
101
92
|
store.save_auth(
|
|
@@ -106,10 +97,10 @@ class CliCommandTests(unittest.TestCase):
|
|
|
106
97
|
session=AuthSession(access_token="token"),
|
|
107
98
|
)
|
|
108
99
|
)
|
|
109
|
-
args = Namespace(config_dir=temp_dir,
|
|
100
|
+
args = Namespace(config_dir=temp_dir, json=False)
|
|
110
101
|
resolved = shared_resolve_config(args, store)
|
|
111
|
-
self.assertEqual(resolved.base_url,
|
|
112
|
-
self.assertEqual(resolved.anon_key,
|
|
102
|
+
self.assertEqual(resolved.base_url, DEFAULT_BASE_URL)
|
|
103
|
+
self.assertEqual(resolved.anon_key, DEFAULT_ANON_KEY)
|
|
113
104
|
|
|
114
105
|
def test_auth_login_saves_polled_session(self) -> None:
|
|
115
106
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
@@ -117,12 +108,9 @@ class CliCommandTests(unittest.TestCase):
|
|
|
117
108
|
config = AppConfig(base_url="https://console.example.com", anon_key="anon")
|
|
118
109
|
args = Namespace(
|
|
119
110
|
config_dir=temp_dir,
|
|
120
|
-
base_url=None,
|
|
121
|
-
anon_key=None,
|
|
122
111
|
json=True,
|
|
123
112
|
login_timeout=300,
|
|
124
113
|
login_id="login-1",
|
|
125
|
-
insecure=False,
|
|
126
114
|
)
|
|
127
115
|
|
|
128
116
|
with mock.patch("powerbase_cli.commands.auth.build_api", return_value=(store, config, None, None, FakeApi())):
|
|
@@ -142,10 +130,7 @@ class CliCommandTests(unittest.TestCase):
|
|
|
142
130
|
config = AppConfig(base_url="https://console.example.com", anon_key="anon")
|
|
143
131
|
args = Namespace(
|
|
144
132
|
config_dir=temp_dir,
|
|
145
|
-
base_url=None,
|
|
146
|
-
anon_key=None,
|
|
147
133
|
json=True,
|
|
148
|
-
insecure=False,
|
|
149
134
|
)
|
|
150
135
|
|
|
151
136
|
with mock.patch("powerbase_cli.commands.auth.build_api", return_value=(store, config, None, None, FakeApiStartOnly())):
|
|
@@ -175,12 +160,9 @@ class CliCommandTests(unittest.TestCase):
|
|
|
175
160
|
config = AppConfig(base_url="https://console.example.com", anon_key="anon")
|
|
176
161
|
args = Namespace(
|
|
177
162
|
config_dir=temp_dir,
|
|
178
|
-
base_url=None,
|
|
179
|
-
anon_key=None,
|
|
180
163
|
json=False,
|
|
181
164
|
login_timeout=5,
|
|
182
165
|
login_id="login-pending",
|
|
183
|
-
insecure=False,
|
|
184
166
|
)
|
|
185
167
|
|
|
186
168
|
with mock.patch("powerbase_cli.commands.auth.build_api", return_value=(store, config, None, None, FakeApiPending())):
|
|
@@ -197,10 +179,7 @@ class CliCommandTests(unittest.TestCase):
|
|
|
197
179
|
def test_agent_chat_streams_jsonl(self) -> None:
|
|
198
180
|
args = Namespace(
|
|
199
181
|
config_dir=None,
|
|
200
|
-
base_url=None,
|
|
201
|
-
anon_key=None,
|
|
202
182
|
json=False,
|
|
203
|
-
insecure=False,
|
|
204
183
|
instance_id="inst-1",
|
|
205
184
|
provider="cursor",
|
|
206
185
|
session_id="sess-1",
|
|
@@ -21,8 +21,8 @@ class CliHelpTests(unittest.TestCase):
|
|
|
21
21
|
self.assertIn("Operate Powerbase console workflows", help_text)
|
|
22
22
|
self.assertIn("agent-driven development", help_text)
|
|
23
23
|
self.assertIn("default implementation interface", help_text)
|
|
24
|
-
self.assertIn("--
|
|
25
|
-
self.assertIn("--
|
|
24
|
+
self.assertIn("--config-dir", help_text)
|
|
25
|
+
self.assertIn("--json", help_text)
|
|
26
26
|
|
|
27
27
|
def test_auth_login_help_mentions_wait_follow_up(self) -> None:
|
|
28
28
|
parser = build_parser()
|
|
@@ -142,8 +142,8 @@ class CliHelpTests(unittest.TestCase):
|
|
|
142
142
|
self.assertIn("release", help_text)
|
|
143
143
|
|
|
144
144
|
def test_normalize_global_argv_moves_root_flags_forward(self) -> None:
|
|
145
|
-
normalized = normalize_global_argv(["auth", "status", "--json", "--config-dir", "/tmp/pb"
|
|
146
|
-
self.assertEqual(normalized, ["--json", "--config-dir", "/tmp/pb", "
|
|
145
|
+
normalized = normalize_global_argv(["auth", "status", "--json", "--config-dir", "/tmp/pb"])
|
|
146
|
+
self.assertEqual(normalized, ["--json", "--config-dir", "/tmp/pb", "auth", "status"])
|
|
147
147
|
|
|
148
148
|
def test_main_normalizes_sys_argv_when_called_without_explicit_args(self) -> None:
|
|
149
149
|
with mock.patch.object(sys, "argv", ["powerbase", "auth", "status", "--json"]):
|
|
@@ -17,18 +17,11 @@ from powerbase_cli.config import (
|
|
|
17
17
|
AuthState,
|
|
18
18
|
ConfigStore,
|
|
19
19
|
ContextState,
|
|
20
|
-
load_bundled_ca_cert,
|
|
21
|
-
merge_config_with_env,
|
|
22
20
|
merge_context_with_env,
|
|
23
21
|
)
|
|
24
22
|
|
|
25
23
|
|
|
26
24
|
class ConfigStoreTests(unittest.TestCase):
|
|
27
|
-
def test_load_bundled_ca_cert_returns_pem(self) -> None:
|
|
28
|
-
cert_pem = load_bundled_ca_cert()
|
|
29
|
-
assert cert_pem is not None
|
|
30
|
-
self.assertIn("BEGIN CERTIFICATE", cert_pem)
|
|
31
|
-
|
|
32
25
|
def test_app_config_uses_builtin_service_defaults(self) -> None:
|
|
33
26
|
config = AppConfig()
|
|
34
27
|
self.assertEqual(config.base_url, DEFAULT_BASE_URL)
|
|
@@ -76,18 +69,11 @@ class ConfigStoreTests(unittest.TestCase):
|
|
|
76
69
|
self.assertEqual(loaded.org_id, "o1")
|
|
77
70
|
self.assertEqual(loaded.branch, "main")
|
|
78
71
|
|
|
79
|
-
def
|
|
72
|
+
def test_merge_context_env_overrides(self) -> None:
|
|
80
73
|
old = dict(os.environ)
|
|
81
74
|
try:
|
|
82
|
-
os.environ["POWERBASE_BASE_URL"] = "https://env.example.com"
|
|
83
75
|
os.environ["POWERBASE_INSTANCE_ID"] = "env-instance"
|
|
84
|
-
os.environ["POWERBASE_TLS_INSECURE"] = "true"
|
|
85
|
-
os.environ["POWERBASE_CA_CERT_FILE"] = "/tmp/test-ca.pem"
|
|
86
|
-
merged_config = merge_config_with_env(AppConfig(base_url="https://file.example.com", anon_key="a"))
|
|
87
76
|
merged_context = merge_context_with_env(ContextState(instance_id="file-instance", branch="main"))
|
|
88
|
-
self.assertEqual(merged_config.base_url, "https://env.example.com")
|
|
89
|
-
self.assertTrue(merged_config.tls_insecure)
|
|
90
|
-
self.assertEqual(merged_config.ca_cert_file, "/tmp/test-ca.pem")
|
|
91
77
|
self.assertEqual(merged_context.instance_id, "env-instance")
|
|
92
78
|
finally:
|
|
93
79
|
os.environ.clear()
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import tempfile
|
|
7
|
+
import unittest
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from unittest import mock
|
|
10
|
+
|
|
11
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
|
12
|
+
|
|
13
|
+
from powerbase_cli.config import AuthSession, AuthState, ConfigStore
|
|
14
|
+
from powerbase_cli.session import SessionError, SessionManager
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class FakeResponse:
|
|
18
|
+
def __init__(self, payload: dict[str, object]) -> None:
|
|
19
|
+
self.payload = payload
|
|
20
|
+
|
|
21
|
+
def __enter__(self) -> "FakeResponse":
|
|
22
|
+
return self
|
|
23
|
+
|
|
24
|
+
def __exit__(self, exc_type, exc, tb) -> None:
|
|
25
|
+
return None
|
|
26
|
+
|
|
27
|
+
def read(self) -> bytes:
|
|
28
|
+
return json.dumps(self.payload).encode("utf-8")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class SessionManagerTests(unittest.TestCase):
|
|
32
|
+
def test_refresh_without_auth_instructs_user_to_log_in_again(self) -> None:
|
|
33
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
34
|
+
store = ConfigStore(Path(temp_dir))
|
|
35
|
+
manager = SessionManager(store, "https://console.example.com", "anon")
|
|
36
|
+
|
|
37
|
+
with self.assertRaises(SessionError) as ctx:
|
|
38
|
+
manager.refresh()
|
|
39
|
+
|
|
40
|
+
self.assertIn("No authentication session available", str(ctx.exception))
|
|
41
|
+
self.assertIn("powerbase auth login --json", str(ctx.exception))
|
|
42
|
+
self.assertIn("powerbase auth wait --login-id ... --json", str(ctx.exception))
|
|
43
|
+
|
|
44
|
+
def test_refresh_updates_saved_auth_file(self) -> None:
|
|
45
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
46
|
+
store = ConfigStore(Path(temp_dir))
|
|
47
|
+
store.save_auth(
|
|
48
|
+
AuthState(
|
|
49
|
+
source="login",
|
|
50
|
+
base_url="https://console.example.com",
|
|
51
|
+
anon_key="anon",
|
|
52
|
+
session=AuthSession(
|
|
53
|
+
access_token="old-access",
|
|
54
|
+
refresh_token="old-refresh",
|
|
55
|
+
expires_at=1,
|
|
56
|
+
),
|
|
57
|
+
)
|
|
58
|
+
)
|
|
59
|
+
manager = SessionManager(store, "https://console.example.com", "anon")
|
|
60
|
+
|
|
61
|
+
with mock.patch("powerbase_cli.session.request.urlopen", return_value=FakeResponse({
|
|
62
|
+
"access_token": "new-access",
|
|
63
|
+
"refresh_token": "new-refresh",
|
|
64
|
+
"expires_at": 9999999999,
|
|
65
|
+
"token_type": "bearer",
|
|
66
|
+
"user": {"id": "user-1"},
|
|
67
|
+
})):
|
|
68
|
+
refreshed = manager.refresh()
|
|
69
|
+
|
|
70
|
+
self.assertEqual(refreshed.session.access_token, "new-access")
|
|
71
|
+
saved = store.load_auth()
|
|
72
|
+
assert saved is not None
|
|
73
|
+
self.assertEqual(saved.session.refresh_token, "new-refresh")
|
|
74
|
+
|
|
75
|
+
def test_env_session_can_refresh_without_writing_auth_file(self) -> None:
|
|
76
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
77
|
+
store = ConfigStore(Path(temp_dir))
|
|
78
|
+
manager = SessionManager(store, "https://console.example.com", "anon")
|
|
79
|
+
old_env = dict(os.environ)
|
|
80
|
+
try:
|
|
81
|
+
os.environ["POWERBASE_ACCESS_TOKEN"] = "env-access"
|
|
82
|
+
os.environ["POWERBASE_REFRESH_TOKEN"] = "env-refresh"
|
|
83
|
+
os.environ["POWERBASE_EXPIRES_AT"] = "1"
|
|
84
|
+
with mock.patch("powerbase_cli.session.request.urlopen", return_value=FakeResponse({
|
|
85
|
+
"access_token": "env-new-access",
|
|
86
|
+
"refresh_token": "env-new-refresh",
|
|
87
|
+
"expires_at": 9999999999,
|
|
88
|
+
"token_type": "bearer",
|
|
89
|
+
})):
|
|
90
|
+
refreshed = manager.ensure_valid()
|
|
91
|
+
assert refreshed is not None
|
|
92
|
+
self.assertEqual(refreshed.session.access_token, "env-new-access")
|
|
93
|
+
self.assertFalse(store.auth_path.exists())
|
|
94
|
+
finally:
|
|
95
|
+
os.environ.clear()
|
|
96
|
+
os.environ.update(old_env)
|
|
97
|
+
|
|
@@ -11,7 +11,7 @@ from urllib.error import HTTPError, URLError
|
|
|
11
11
|
|
|
12
12
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
|
13
13
|
|
|
14
|
-
from powerbase_cli.config import
|
|
14
|
+
from powerbase_cli.config import AppConfig, AuthSession, AuthState, ConfigStore
|
|
15
15
|
from powerbase_cli.session import SessionManager
|
|
16
16
|
from powerbase_cli.transport import ApiError, PowerbaseTransport
|
|
17
17
|
|
|
@@ -51,71 +51,6 @@ class PowerbaseTransportTests(unittest.TestCase):
|
|
|
51
51
|
self.assertIn("powerbase auth wait --login-id ... --json", str(ctx.exception))
|
|
52
52
|
self.assertEqual(urlopen_mock.call_count, 0)
|
|
53
53
|
|
|
54
|
-
def test_invoke_uses_unverified_tls_context_when_insecure(self) -> None:
|
|
55
|
-
with tempfile.TemporaryDirectory() as temp_dir:
|
|
56
|
-
store = ConfigStore(Path(temp_dir))
|
|
57
|
-
manager = SessionManager(store, "https://console.example.com", "anon", tls_insecure=True)
|
|
58
|
-
transport = PowerbaseTransport(
|
|
59
|
-
AppConfig(base_url="https://console.example.com", anon_key="anon", tls_insecure=True),
|
|
60
|
-
manager,
|
|
61
|
-
)
|
|
62
|
-
|
|
63
|
-
with mock.patch(
|
|
64
|
-
"powerbase_cli.transport.request.urlopen",
|
|
65
|
-
return_value=FakeResponse({"success": True, "data": {"ok": True}}),
|
|
66
|
-
) as urlopen_mock:
|
|
67
|
-
result = transport.invoke("instances", method="GET", requires_auth=False)
|
|
68
|
-
|
|
69
|
-
self.assertEqual(result["data"]["ok"], True)
|
|
70
|
-
context = urlopen_mock.call_args.kwargs["context"]
|
|
71
|
-
self.assertFalse(context.check_hostname)
|
|
72
|
-
|
|
73
|
-
def test_invoke_uses_ca_cert_context_when_configured(self) -> None:
|
|
74
|
-
with tempfile.TemporaryDirectory() as temp_dir:
|
|
75
|
-
store = ConfigStore(Path(temp_dir))
|
|
76
|
-
manager = SessionManager(store, "https://console.example.com", "anon", ca_cert_file="/tmp/test-ca.pem")
|
|
77
|
-
transport = PowerbaseTransport(
|
|
78
|
-
AppConfig(base_url="https://console.example.com", anon_key="anon", ca_cert_file="/tmp/test-ca.pem"),
|
|
79
|
-
manager,
|
|
80
|
-
)
|
|
81
|
-
|
|
82
|
-
with mock.patch("powerbase_cli.transport.ssl.create_default_context") as context_factory:
|
|
83
|
-
fake_context = object()
|
|
84
|
-
context_factory.return_value = fake_context
|
|
85
|
-
with mock.patch(
|
|
86
|
-
"powerbase_cli.transport.request.urlopen",
|
|
87
|
-
return_value=FakeResponse({"success": True, "data": {"ok": True}}),
|
|
88
|
-
) as urlopen_mock:
|
|
89
|
-
result = transport.invoke("instances", method="GET", requires_auth=False)
|
|
90
|
-
|
|
91
|
-
self.assertEqual(result["data"]["ok"], True)
|
|
92
|
-
context_factory.assert_called_once_with(cafile="/tmp/test-ca.pem")
|
|
93
|
-
self.assertIs(urlopen_mock.call_args.kwargs["context"], fake_context)
|
|
94
|
-
|
|
95
|
-
def test_invoke_uses_bundled_ca_cert_when_configured(self) -> None:
|
|
96
|
-
with tempfile.TemporaryDirectory() as temp_dir:
|
|
97
|
-
store = ConfigStore(Path(temp_dir))
|
|
98
|
-
manager = SessionManager(store, "https://console.example.com", "anon", ca_cert_file=BUNDLED_CA_CERT_SENTINEL)
|
|
99
|
-
transport = PowerbaseTransport(
|
|
100
|
-
AppConfig(base_url="https://console.example.com", anon_key="anon", ca_cert_file=BUNDLED_CA_CERT_SENTINEL),
|
|
101
|
-
manager,
|
|
102
|
-
)
|
|
103
|
-
|
|
104
|
-
with mock.patch("powerbase_cli.transport.load_bundled_ca_cert", return_value="PEM DATA") as bundled_cert:
|
|
105
|
-
with mock.patch("powerbase_cli.transport.ssl.create_default_context") as context_factory:
|
|
106
|
-
fake_context = object()
|
|
107
|
-
context_factory.return_value = fake_context
|
|
108
|
-
with mock.patch(
|
|
109
|
-
"powerbase_cli.transport.request.urlopen",
|
|
110
|
-
return_value=FakeResponse({"success": True, "data": {"ok": True}}),
|
|
111
|
-
) as urlopen_mock:
|
|
112
|
-
result = transport.invoke("instances", method="GET", requires_auth=False)
|
|
113
|
-
|
|
114
|
-
self.assertEqual(result["data"]["ok"], True)
|
|
115
|
-
bundled_cert.assert_called_once_with()
|
|
116
|
-
context_factory.assert_called_once_with(cadata="PEM DATA")
|
|
117
|
-
self.assertIs(urlopen_mock.call_args.kwargs["context"], fake_context)
|
|
118
|
-
|
|
119
54
|
def test_invoke_refreshes_and_retries_once_on_401(self) -> None:
|
|
120
55
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
121
56
|
store = ConfigStore(Path(temp_dir))
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
-----BEGIN CERTIFICATE-----
|
|
2
|
-
MIIDhDCCAmygAwIBAgIUWu1JfeyC2qw56+7PUB5QY9w1MG8wDQYJKoZIhvcNAQEL
|
|
3
|
-
BQAwHjEcMBoGA1UEAwwTNi4xMi4yMzUuMTY1Lm5pcC5pbzAeFw0yNjA0MDcwNTUx
|
|
4
|
-
MjZaFw0yNzA0MDcwNTUxMjZaMB4xHDAaBgNVBAMMEzYuMTIuMjM1LjE2NS5uaXAu
|
|
5
|
-
aW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDVaa9eQzG9t/WNcoP8
|
|
6
|
-
vJDX41oELoz/LOVzmr9zuFK3jQnIrVjFW8MdBHq1pOBLyHjKtnhKPo1n0br4/iPP
|
|
7
|
-
/TiJNwAuybN6iiSxWSsGpzFv7gIyJivdr65khzeLNda6HyBFxsRsVZAKKe8tueB4
|
|
8
|
-
FQM0YWxHkfAMw5bigDumZq3wzLUuvB7EtscVtl0dV0tcqovLq1Lg9/eBDSqfWuxv
|
|
9
|
-
hW9iROv23ryJETE/8Wh/2Gd5UV0E8CEX8As30+5nJ2jGF6O8abDRipvqc67+5v7h
|
|
10
|
-
dEXT2xIg4Fh8DAhlbgzM6Sj6I70pWiro+Q+N8GWvpJeJYGZu7J97eEOw+u1C2pCV
|
|
11
|
-
sbiBAgMBAAGjgbkwgbYwHQYDVR0OBBYEFODuAm1rmrl2GyZBcClH9HVE9kGMMB8G
|
|
12
|
-
A1UdIwQYMBaAFODuAm1rmrl2GyZBcClH9HVE9kGMMA8GA1UdEwEB/wQFMAMBAf8w
|
|
13
|
-
YwYDVR0RBFwwWoITNi4xMi4yMzUuMTY1Lm5pcC5pb4IbY29uc29sZS42LjEyLjIz
|
|
14
|
-
NS4xNjUubmlwLmlvghUqLjYuMTIuMjM1LjE2NS5uaXAuaW+CCWxvY2FsaG9zdIcE
|
|
15
|
-
fwAAATANBgkqhkiG9w0BAQsFAAOCAQEAmvCeJ4Xnx8rXmU+oxiDiDZRfUK204Ta7
|
|
16
|
-
hztXu9LvxheW99p2R1AP2F7VUJOTq31HY9/r2p3qCEyz8F4nc/GPunHfF3y8mMiB
|
|
17
|
-
beFDdwrw8NWTRLGlOJynjsOXtkRkFa03DTKa4x9roZo10S+imDk1/DywWlv7UoAK
|
|
18
|
-
9OsUB1zb8c6m7uZXrOTAZOzvO2fBrdymXnQ4tMvl54iyVT8X1rft5NUywfBzCtzB
|
|
19
|
-
g/OxLM2zJg3kz9kFztRuf1e07Tz6cqSy2CzClvhxaYJSfnlT2b73SceKy3kHn7Sh
|
|
20
|
-
VXA8yjqcTjk6wkMlHueIWnjnjYkRSpFjw3/ODNXbGJjxh+86l/ekPQ==
|
|
21
|
-
-----END CERTIFICATE-----
|
|
@@ -1,194 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
import os
|
|
5
|
-
import sys
|
|
6
|
-
import tempfile
|
|
7
|
-
import unittest
|
|
8
|
-
from pathlib import Path
|
|
9
|
-
from unittest import mock
|
|
10
|
-
|
|
11
|
-
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
|
12
|
-
|
|
13
|
-
from powerbase_cli.config import BUNDLED_CA_CERT_SENTINEL, AuthSession, AuthState, ConfigStore
|
|
14
|
-
from powerbase_cli.session import SessionError, SessionManager
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
class FakeResponse:
|
|
18
|
-
def __init__(self, payload: dict[str, object]) -> None:
|
|
19
|
-
self.payload = payload
|
|
20
|
-
|
|
21
|
-
def __enter__(self) -> "FakeResponse":
|
|
22
|
-
return self
|
|
23
|
-
|
|
24
|
-
def __exit__(self, exc_type, exc, tb) -> None:
|
|
25
|
-
return None
|
|
26
|
-
|
|
27
|
-
def read(self) -> bytes:
|
|
28
|
-
return json.dumps(self.payload).encode("utf-8")
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
class SessionManagerTests(unittest.TestCase):
|
|
32
|
-
def test_refresh_without_auth_instructs_user_to_log_in_again(self) -> None:
|
|
33
|
-
with tempfile.TemporaryDirectory() as temp_dir:
|
|
34
|
-
store = ConfigStore(Path(temp_dir))
|
|
35
|
-
manager = SessionManager(store, "https://console.example.com", "anon")
|
|
36
|
-
|
|
37
|
-
with self.assertRaises(SessionError) as ctx:
|
|
38
|
-
manager.refresh()
|
|
39
|
-
|
|
40
|
-
self.assertIn("No authentication session available", str(ctx.exception))
|
|
41
|
-
self.assertIn("powerbase auth login --json", str(ctx.exception))
|
|
42
|
-
self.assertIn("powerbase auth wait --login-id ... --json", str(ctx.exception))
|
|
43
|
-
|
|
44
|
-
def test_refresh_updates_saved_auth_file(self) -> None:
|
|
45
|
-
with tempfile.TemporaryDirectory() as temp_dir:
|
|
46
|
-
store = ConfigStore(Path(temp_dir))
|
|
47
|
-
store.save_auth(
|
|
48
|
-
AuthState(
|
|
49
|
-
source="login",
|
|
50
|
-
base_url="https://console.example.com",
|
|
51
|
-
anon_key="anon",
|
|
52
|
-
session=AuthSession(
|
|
53
|
-
access_token="old-access",
|
|
54
|
-
refresh_token="old-refresh",
|
|
55
|
-
expires_at=1,
|
|
56
|
-
),
|
|
57
|
-
)
|
|
58
|
-
)
|
|
59
|
-
manager = SessionManager(store, "https://console.example.com", "anon")
|
|
60
|
-
|
|
61
|
-
with mock.patch("powerbase_cli.session.request.urlopen", return_value=FakeResponse({
|
|
62
|
-
"access_token": "new-access",
|
|
63
|
-
"refresh_token": "new-refresh",
|
|
64
|
-
"expires_at": 9999999999,
|
|
65
|
-
"token_type": "bearer",
|
|
66
|
-
"user": {"id": "user-1"},
|
|
67
|
-
})):
|
|
68
|
-
refreshed = manager.refresh()
|
|
69
|
-
|
|
70
|
-
self.assertEqual(refreshed.session.access_token, "new-access")
|
|
71
|
-
saved = store.load_auth()
|
|
72
|
-
assert saved is not None
|
|
73
|
-
self.assertEqual(saved.session.refresh_token, "new-refresh")
|
|
74
|
-
|
|
75
|
-
def test_env_session_can_refresh_without_writing_auth_file(self) -> None:
|
|
76
|
-
with tempfile.TemporaryDirectory() as temp_dir:
|
|
77
|
-
store = ConfigStore(Path(temp_dir))
|
|
78
|
-
manager = SessionManager(store, "https://console.example.com", "anon")
|
|
79
|
-
old_env = dict(os.environ)
|
|
80
|
-
try:
|
|
81
|
-
os.environ["POWERBASE_ACCESS_TOKEN"] = "env-access"
|
|
82
|
-
os.environ["POWERBASE_REFRESH_TOKEN"] = "env-refresh"
|
|
83
|
-
os.environ["POWERBASE_EXPIRES_AT"] = "1"
|
|
84
|
-
os.environ["POWERBASE_BASE_URL"] = "https://console.example.com"
|
|
85
|
-
os.environ["POWERBASE_ANON_KEY"] = "anon"
|
|
86
|
-
with mock.patch("powerbase_cli.session.request.urlopen", return_value=FakeResponse({
|
|
87
|
-
"access_token": "env-new-access",
|
|
88
|
-
"refresh_token": "env-new-refresh",
|
|
89
|
-
"expires_at": 9999999999,
|
|
90
|
-
"token_type": "bearer",
|
|
91
|
-
})):
|
|
92
|
-
refreshed = manager.ensure_valid()
|
|
93
|
-
assert refreshed is not None
|
|
94
|
-
self.assertEqual(refreshed.session.access_token, "env-new-access")
|
|
95
|
-
self.assertFalse(store.auth_path.exists())
|
|
96
|
-
finally:
|
|
97
|
-
os.environ.clear()
|
|
98
|
-
os.environ.update(old_env)
|
|
99
|
-
|
|
100
|
-
def test_refresh_uses_unverified_tls_context_when_insecure(self) -> None:
|
|
101
|
-
with tempfile.TemporaryDirectory() as temp_dir:
|
|
102
|
-
store = ConfigStore(Path(temp_dir))
|
|
103
|
-
store.save_auth(
|
|
104
|
-
AuthState(
|
|
105
|
-
source="login",
|
|
106
|
-
base_url="https://console.example.com",
|
|
107
|
-
anon_key="anon",
|
|
108
|
-
session=AuthSession(access_token="old-access", refresh_token="old-refresh", expires_at=1),
|
|
109
|
-
)
|
|
110
|
-
)
|
|
111
|
-
manager = SessionManager(store, "https://console.example.com", "anon", tls_insecure=True)
|
|
112
|
-
|
|
113
|
-
with mock.patch(
|
|
114
|
-
"powerbase_cli.session.request.urlopen",
|
|
115
|
-
return_value=FakeResponse(
|
|
116
|
-
{
|
|
117
|
-
"access_token": "new-access",
|
|
118
|
-
"refresh_token": "new-refresh",
|
|
119
|
-
"expires_at": 9999999999,
|
|
120
|
-
"token_type": "bearer",
|
|
121
|
-
}
|
|
122
|
-
),
|
|
123
|
-
) as urlopen_mock:
|
|
124
|
-
manager.refresh()
|
|
125
|
-
|
|
126
|
-
context = urlopen_mock.call_args.kwargs["context"]
|
|
127
|
-
self.assertFalse(context.check_hostname)
|
|
128
|
-
|
|
129
|
-
def test_refresh_uses_ca_cert_context_when_configured(self) -> None:
|
|
130
|
-
with tempfile.TemporaryDirectory() as temp_dir:
|
|
131
|
-
store = ConfigStore(Path(temp_dir))
|
|
132
|
-
store.save_auth(
|
|
133
|
-
AuthState(
|
|
134
|
-
source="login",
|
|
135
|
-
base_url="https://console.example.com",
|
|
136
|
-
anon_key="anon",
|
|
137
|
-
session=AuthSession(access_token="old-access", refresh_token="old-refresh", expires_at=1),
|
|
138
|
-
)
|
|
139
|
-
)
|
|
140
|
-
manager = SessionManager(store, "https://console.example.com", "anon", ca_cert_file="/tmp/test-ca.pem")
|
|
141
|
-
|
|
142
|
-
with mock.patch("powerbase_cli.session.ssl.create_default_context") as context_factory:
|
|
143
|
-
fake_context = object()
|
|
144
|
-
context_factory.return_value = fake_context
|
|
145
|
-
with mock.patch(
|
|
146
|
-
"powerbase_cli.session.request.urlopen",
|
|
147
|
-
return_value=FakeResponse(
|
|
148
|
-
{
|
|
149
|
-
"access_token": "new-access",
|
|
150
|
-
"refresh_token": "new-refresh",
|
|
151
|
-
"expires_at": 9999999999,
|
|
152
|
-
"token_type": "bearer",
|
|
153
|
-
}
|
|
154
|
-
),
|
|
155
|
-
) as urlopen_mock:
|
|
156
|
-
manager.refresh()
|
|
157
|
-
|
|
158
|
-
context_factory.assert_called_once_with(cafile="/tmp/test-ca.pem")
|
|
159
|
-
self.assertIs(urlopen_mock.call_args.kwargs["context"], fake_context)
|
|
160
|
-
|
|
161
|
-
def test_refresh_uses_bundled_ca_cert_when_configured(self) -> None:
|
|
162
|
-
with tempfile.TemporaryDirectory() as temp_dir:
|
|
163
|
-
store = ConfigStore(Path(temp_dir))
|
|
164
|
-
store.save_auth(
|
|
165
|
-
AuthState(
|
|
166
|
-
source="login",
|
|
167
|
-
base_url="https://console.example.com",
|
|
168
|
-
anon_key="anon",
|
|
169
|
-
session=AuthSession(access_token="old-access", refresh_token="old-refresh", expires_at=1),
|
|
170
|
-
)
|
|
171
|
-
)
|
|
172
|
-
manager = SessionManager(store, "https://console.example.com", "anon", ca_cert_file=BUNDLED_CA_CERT_SENTINEL)
|
|
173
|
-
|
|
174
|
-
with mock.patch("powerbase_cli.session.load_bundled_ca_cert", return_value="PEM DATA") as bundled_cert:
|
|
175
|
-
with mock.patch("powerbase_cli.session.ssl.create_default_context") as context_factory:
|
|
176
|
-
fake_context = object()
|
|
177
|
-
context_factory.return_value = fake_context
|
|
178
|
-
with mock.patch(
|
|
179
|
-
"powerbase_cli.session.request.urlopen",
|
|
180
|
-
return_value=FakeResponse(
|
|
181
|
-
{
|
|
182
|
-
"access_token": "new-access",
|
|
183
|
-
"refresh_token": "new-refresh",
|
|
184
|
-
"expires_at": 9999999999,
|
|
185
|
-
"token_type": "bearer",
|
|
186
|
-
}
|
|
187
|
-
),
|
|
188
|
-
) as urlopen_mock:
|
|
189
|
-
manager.refresh()
|
|
190
|
-
|
|
191
|
-
bundled_cert.assert_called_once_with()
|
|
192
|
-
context_factory.assert_called_once_with(cadata="PEM DATA")
|
|
193
|
-
self.assertIs(urlopen_mock.call_args.kwargs["context"], fake_context)
|
|
194
|
-
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|