movitera-cli 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- movitera_cli-0.1.0/.github/workflows/publish.yml +22 -0
- movitera_cli-0.1.0/.github/workflows/test.yml +18 -0
- movitera_cli-0.1.0/.gitignore +35 -0
- movitera_cli-0.1.0/LICENSE +21 -0
- movitera_cli-0.1.0/PKG-INFO +113 -0
- movitera_cli-0.1.0/README.md +84 -0
- movitera_cli-0.1.0/movitera_cli/__init__.py +25 -0
- movitera_cli-0.1.0/movitera_cli/client.py +131 -0
- movitera_cli-0.1.0/movitera_cli/config.py +22 -0
- movitera_cli-0.1.0/movitera_cli/main.py +307 -0
- movitera_cli-0.1.0/movitera_cli/token_store.py +94 -0
- movitera_cli-0.1.0/pyproject.toml +68 -0
- movitera_cli-0.1.0/tests/test_client.py +100 -0
- movitera_cli-0.1.0/tests/test_commands.py +196 -0
- movitera_cli-0.1.0/tests/test_dotenv_parse.py +45 -0
- movitera_cli-0.1.0/tests/test_token_store.py +82 -0
- movitera_cli-0.1.0/uv.lock +579 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ['v*.*.*']
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
publish:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
permissions:
|
|
11
|
+
contents: read
|
|
12
|
+
id-token: write
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
- uses: astral-sh/setup-uv@v3
|
|
16
|
+
with:
|
|
17
|
+
python-version: "3.13"
|
|
18
|
+
- run: uv sync
|
|
19
|
+
- run: uv run pytest
|
|
20
|
+
- run: uv run mypy movitera_cli/
|
|
21
|
+
- run: uv build
|
|
22
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
name: Test
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
- uses: astral-sh/setup-uv@v3
|
|
14
|
+
with:
|
|
15
|
+
python-version: "3.13"
|
|
16
|
+
- run: uv sync
|
|
17
|
+
- run: uv run pytest
|
|
18
|
+
- run: uv run mypy movitera_cli/
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
|
|
8
|
+
# Virtual environments
|
|
9
|
+
.venv/
|
|
10
|
+
venv/
|
|
11
|
+
env/
|
|
12
|
+
|
|
13
|
+
# Distribution / packaging
|
|
14
|
+
dist/
|
|
15
|
+
build/
|
|
16
|
+
*.egg-info/
|
|
17
|
+
*.egg
|
|
18
|
+
|
|
19
|
+
# Test / coverage
|
|
20
|
+
.pytest_cache/
|
|
21
|
+
.mypy_cache/
|
|
22
|
+
.coverage
|
|
23
|
+
.coverage.*
|
|
24
|
+
coverage.xml
|
|
25
|
+
htmlcov/
|
|
26
|
+
|
|
27
|
+
# Editor / OS
|
|
28
|
+
.idea/
|
|
29
|
+
.vscode/
|
|
30
|
+
*.swp
|
|
31
|
+
.DS_Store
|
|
32
|
+
|
|
33
|
+
# Local env
|
|
34
|
+
.env
|
|
35
|
+
.env.local
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Movitera
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: movitera-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Movitera CLI: inject vault secrets as environment variables, generate TOTP codes
|
|
5
|
+
Project-URL: Homepage, https://github.com/joaoheusi/movitera-cli
|
|
6
|
+
Project-URL: Repository, https://github.com/joaoheusi/movitera-cli
|
|
7
|
+
Project-URL: Issues, https://github.com/joaoheusi/movitera-cli/issues
|
|
8
|
+
Author: Movitera
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: cli,dotenv,movitera,password-manager,secrets,totp,vault
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: System Administrators
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Security
|
|
22
|
+
Classifier: Topic :: System :: Systems Administration
|
|
23
|
+
Requires-Python: <4,>=3.11
|
|
24
|
+
Requires-Dist: click<9,>=8.1
|
|
25
|
+
Requires-Dist: httpx<1,>=0.28.1
|
|
26
|
+
Requires-Dist: keyring<26,>=25
|
|
27
|
+
Requires-Dist: pyotp<3,>=2.9
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# movitera CLI
|
|
31
|
+
|
|
32
|
+
Inject Movitera vault secrets as environment variables, render dotenv files, and generate TOTP codes from the terminal. Authenticates with a Movitera vault personal access token (PAT).
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install movitera-cli
|
|
38
|
+
# or install as an isolated CLI tool (recommended):
|
|
39
|
+
pipx install movitera-cli
|
|
40
|
+
# or with uv:
|
|
41
|
+
uv tool install movitera-cli
|
|
42
|
+
# or run without installing:
|
|
43
|
+
uvx --from movitera-cli movitera --help
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Quick start
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
# 1. Mint a PAT in the web app (Settings → Vault → Access Tokens),
|
|
50
|
+
# then paste it once:
|
|
51
|
+
movitera login
|
|
52
|
+
|
|
53
|
+
# 2. Run any command with secrets injected as env vars:
|
|
54
|
+
movitera run --team <teamId> --credential myapp-prod -- npm start
|
|
55
|
+
|
|
56
|
+
# 3. Or dump as dotenv:
|
|
57
|
+
movitera secrets pull --team <teamId> --credential myapp-prod > .env
|
|
58
|
+
|
|
59
|
+
# 4. Or generate a TOTP code for a credential:
|
|
60
|
+
movitera totp --team <teamId> <credential-id>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## How it works
|
|
64
|
+
|
|
65
|
+
`movitera run -- cmd` fetches the dotenv body from
|
|
66
|
+
`GET /vault/credentials/by-name/{name}/env`, then `exec`s the child process
|
|
67
|
+
with those vars merged into its environment. The CLI never writes secrets
|
|
68
|
+
to disk — they exist only in the child process's `environ`.
|
|
69
|
+
|
|
70
|
+
Tokens are stored in the OS keyring (Keychain on macOS, libsecret on Linux,
|
|
71
|
+
Credential Manager on Windows) with a file fallback under
|
|
72
|
+
`~/.config/movitera/token` (mode `0o600`) if no keyring backend is
|
|
73
|
+
available.
|
|
74
|
+
|
|
75
|
+
## Config
|
|
76
|
+
|
|
77
|
+
| Env var | Purpose | Default |
|
|
78
|
+
|---|---|---|
|
|
79
|
+
| `MOVITERA_API_URL` | API base URL | `https://api.movitera.com` |
|
|
80
|
+
| `MOVITERA_TOKEN` | PAT override (skips keyring lookup) | — |
|
|
81
|
+
| `MOVITERA_TEAM` | Default team id (you can omit `--team`) | — |
|
|
82
|
+
|
|
83
|
+
## Commands
|
|
84
|
+
|
|
85
|
+
### `movitera login`
|
|
86
|
+
Prompts for a PAT (or reads it from stdin via `--stdin`) and stores it.
|
|
87
|
+
|
|
88
|
+
### `movitera logout`
|
|
89
|
+
Removes the stored PAT.
|
|
90
|
+
|
|
91
|
+
### `movitera run --team T --credential N -- <cmd> [args...]`
|
|
92
|
+
Fetches the `ENV_BUNDLE` credential named `N` and execs `<cmd>` with those
|
|
93
|
+
vars in the environment.
|
|
94
|
+
|
|
95
|
+
### `movitera secrets pull --team T --credential N`
|
|
96
|
+
Writes the dotenv body to stdout. Exits non-zero on resolution failures so
|
|
97
|
+
you can chain `movitera secrets pull ... > .env`.
|
|
98
|
+
|
|
99
|
+
### `movitera totp --team T <credential-id>`
|
|
100
|
+
Prints the current TOTP code for the credential's `OTPAUTH_URI` field.
|
|
101
|
+
|
|
102
|
+
### `movitera tokens list --team T` / `movitera tokens revoke <id>`
|
|
103
|
+
Manage your own PATs from the CLI.
|
|
104
|
+
|
|
105
|
+
## Security model
|
|
106
|
+
|
|
107
|
+
- PATs have full 256-bit entropy and are stored as SHA-256 on the server,
|
|
108
|
+
so a server compromise cannot recover the plaintext.
|
|
109
|
+
- Every `movitera run` invocation is audited server-side, but PAT reads
|
|
110
|
+
are **coalesced per hour per token** (see the backend's
|
|
111
|
+
`FETCHED_VIA_TOKEN` aggregation) so a hot dev loop doesn't drown the log.
|
|
112
|
+
- Group/team access is re-checked on every request: if your vault scope is
|
|
113
|
+
revoked, your PAT immediately loses access — no token-cache divergence.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# movitera CLI
|
|
2
|
+
|
|
3
|
+
Inject Movitera vault secrets as environment variables, render dotenv files, and generate TOTP codes from the terminal. Authenticates with a Movitera vault personal access token (PAT).
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install movitera-cli
|
|
9
|
+
# or install as an isolated CLI tool (recommended):
|
|
10
|
+
pipx install movitera-cli
|
|
11
|
+
# or with uv:
|
|
12
|
+
uv tool install movitera-cli
|
|
13
|
+
# or run without installing:
|
|
14
|
+
uvx --from movitera-cli movitera --help
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Quick start
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# 1. Mint a PAT in the web app (Settings → Vault → Access Tokens),
|
|
21
|
+
# then paste it once:
|
|
22
|
+
movitera login
|
|
23
|
+
|
|
24
|
+
# 2. Run any command with secrets injected as env vars:
|
|
25
|
+
movitera run --team <teamId> --credential myapp-prod -- npm start
|
|
26
|
+
|
|
27
|
+
# 3. Or dump as dotenv:
|
|
28
|
+
movitera secrets pull --team <teamId> --credential myapp-prod > .env
|
|
29
|
+
|
|
30
|
+
# 4. Or generate a TOTP code for a credential:
|
|
31
|
+
movitera totp --team <teamId> <credential-id>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## How it works
|
|
35
|
+
|
|
36
|
+
`movitera run -- cmd` fetches the dotenv body from
|
|
37
|
+
`GET /vault/credentials/by-name/{name}/env`, then `exec`s the child process
|
|
38
|
+
with those vars merged into its environment. The CLI never writes secrets
|
|
39
|
+
to disk — they exist only in the child process's `environ`.
|
|
40
|
+
|
|
41
|
+
Tokens are stored in the OS keyring (Keychain on macOS, libsecret on Linux,
|
|
42
|
+
Credential Manager on Windows) with a file fallback under
|
|
43
|
+
`~/.config/movitera/token` (mode `0o600`) if no keyring backend is
|
|
44
|
+
available.
|
|
45
|
+
|
|
46
|
+
## Config
|
|
47
|
+
|
|
48
|
+
| Env var | Purpose | Default |
|
|
49
|
+
|---|---|---|
|
|
50
|
+
| `MOVITERA_API_URL` | API base URL | `https://api.movitera.com` |
|
|
51
|
+
| `MOVITERA_TOKEN` | PAT override (skips keyring lookup) | — |
|
|
52
|
+
| `MOVITERA_TEAM` | Default team id (you can omit `--team`) | — |
|
|
53
|
+
|
|
54
|
+
## Commands
|
|
55
|
+
|
|
56
|
+
### `movitera login`
|
|
57
|
+
Prompts for a PAT (or reads it from stdin via `--stdin`) and stores it.
|
|
58
|
+
|
|
59
|
+
### `movitera logout`
|
|
60
|
+
Removes the stored PAT.
|
|
61
|
+
|
|
62
|
+
### `movitera run --team T --credential N -- <cmd> [args...]`
|
|
63
|
+
Fetches the `ENV_BUNDLE` credential named `N` and execs `<cmd>` with those
|
|
64
|
+
vars in the environment.
|
|
65
|
+
|
|
66
|
+
### `movitera secrets pull --team T --credential N`
|
|
67
|
+
Writes the dotenv body to stdout. Exits non-zero on resolution failures so
|
|
68
|
+
you can chain `movitera secrets pull ... > .env`.
|
|
69
|
+
|
|
70
|
+
### `movitera totp --team T <credential-id>`
|
|
71
|
+
Prints the current TOTP code for the credential's `OTPAUTH_URI` field.
|
|
72
|
+
|
|
73
|
+
### `movitera tokens list --team T` / `movitera tokens revoke <id>`
|
|
74
|
+
Manage your own PATs from the CLI.
|
|
75
|
+
|
|
76
|
+
## Security model
|
|
77
|
+
|
|
78
|
+
- PATs have full 256-bit entropy and are stored as SHA-256 on the server,
|
|
79
|
+
so a server compromise cannot recover the plaintext.
|
|
80
|
+
- Every `movitera run` invocation is audited server-side, but PAT reads
|
|
81
|
+
are **coalesced per hour per token** (see the backend's
|
|
82
|
+
`FETCHED_VIA_TOKEN` aggregation) so a hot dev loop doesn't drown the log.
|
|
83
|
+
- Group/team access is re-checked on every request: if your vault scope is
|
|
84
|
+
revoked, your PAT immediately loses access — no token-cache divergence.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Movitera CLI for vault secrets, dotenv export, TOTP codes.
|
|
2
|
+
|
|
3
|
+
Also exposes a small SDK surface for programmatic use:
|
|
4
|
+
|
|
5
|
+
from movitera_cli import VaultClient
|
|
6
|
+
from movitera_cli.token_store import load_token
|
|
7
|
+
|
|
8
|
+
client = VaultClient(api_url="https://api.movitera.com", token=load_token())
|
|
9
|
+
env = client.get_env(team_id="...", name="myapp-prod") # raw dotenv text
|
|
10
|
+
|
|
11
|
+
That's intentionally thin — the API itself is the SDK. We don't wrap it in
|
|
12
|
+
domain objects because the surface evolves with the backend and the wire
|
|
13
|
+
shape is small enough to consume directly.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from movitera_cli.client import AccessTokenSummary, MoviteraError, VaultClient
|
|
17
|
+
|
|
18
|
+
__version__ = "0.1.0"
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"AccessTokenSummary",
|
|
22
|
+
"MoviteraError",
|
|
23
|
+
"VaultClient",
|
|
24
|
+
"__version__",
|
|
25
|
+
]
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""HTTP client wrapping the Movitera vault API for the CLI.
|
|
2
|
+
|
|
3
|
+
Thin wrapper around `httpx` that:
|
|
4
|
+
- Adds the `Authorization: Bearer <pat>` header.
|
|
5
|
+
- Surfaces server `errorCode` strings in raised exceptions so the CLI can
|
|
6
|
+
print actionable messages (e.g. CREDENTIAL_NAME_NOT_UNIQUE → "try --id").
|
|
7
|
+
- Picks `text/plain` vs JSON correctly per endpoint.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
import httpx
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class MoviteraError(Exception):
|
|
19
|
+
"""API call returned a non-2xx response we want to surface to the user."""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
message: str,
|
|
24
|
+
*,
|
|
25
|
+
status_code: int | None = None,
|
|
26
|
+
error_code: str | None = None,
|
|
27
|
+
):
|
|
28
|
+
super().__init__(message)
|
|
29
|
+
self.status_code = status_code
|
|
30
|
+
self.error_code = error_code
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class AccessTokenSummary:
|
|
35
|
+
id: str
|
|
36
|
+
name: str
|
|
37
|
+
prefix: str
|
|
38
|
+
clientKind: str
|
|
39
|
+
createdAt: str
|
|
40
|
+
expiresAt: str | None
|
|
41
|
+
lastUsedAt: str | None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class VaultClient:
|
|
45
|
+
def __init__(self, api_url: str, token: str, *, timeout: float = 30.0):
|
|
46
|
+
self._client = httpx.Client(
|
|
47
|
+
base_url=api_url,
|
|
48
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
49
|
+
timeout=timeout,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
def close(self) -> None:
|
|
53
|
+
self._client.close()
|
|
54
|
+
|
|
55
|
+
def __enter__(self) -> VaultClient:
|
|
56
|
+
return self
|
|
57
|
+
|
|
58
|
+
def __exit__(self, *_: Any) -> None:
|
|
59
|
+
self.close()
|
|
60
|
+
|
|
61
|
+
# ----- env bundle (CLI hot path) ------------------------------------
|
|
62
|
+
|
|
63
|
+
def get_env(self, team_id: str, name: str) -> str:
|
|
64
|
+
"""Return raw dotenv text for the named ENV_BUNDLE credential."""
|
|
65
|
+
response = self._client.get(
|
|
66
|
+
f"/vault/credentials/by-name/{name}/env",
|
|
67
|
+
params={"teamId": team_id},
|
|
68
|
+
)
|
|
69
|
+
self._raise_for_status(response)
|
|
70
|
+
return response.text
|
|
71
|
+
|
|
72
|
+
# ----- TOTP ----------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
def get_totp(self, credential_id: str) -> tuple[str, str]:
|
|
75
|
+
"""Return (code, ISO-8601 expiresAt) from the server-side endpoint."""
|
|
76
|
+
response = self._client.get(f"/vault/credentials/{credential_id}/totp")
|
|
77
|
+
self._raise_for_status(response)
|
|
78
|
+
body = response.json()
|
|
79
|
+
return body["code"], body["expiresAt"]
|
|
80
|
+
|
|
81
|
+
# ----- access tokens (self-service) ---------------------------------
|
|
82
|
+
|
|
83
|
+
def list_access_tokens(self, team_id: str) -> list[AccessTokenSummary]:
|
|
84
|
+
response = self._client.get(
|
|
85
|
+
"/vault/access-tokens", params={"teamId": team_id}
|
|
86
|
+
)
|
|
87
|
+
self._raise_for_status(response)
|
|
88
|
+
return [
|
|
89
|
+
AccessTokenSummary(
|
|
90
|
+
id=item["id"],
|
|
91
|
+
name=item["name"],
|
|
92
|
+
prefix=item["prefix"],
|
|
93
|
+
clientKind=item["clientKind"],
|
|
94
|
+
createdAt=item["createdAt"],
|
|
95
|
+
expiresAt=item.get("expiresAt"),
|
|
96
|
+
lastUsedAt=item.get("lastUsedAt"),
|
|
97
|
+
)
|
|
98
|
+
for item in response.json()
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
def revoke_access_token(self, token_id: str) -> None:
|
|
102
|
+
response = self._client.delete(f"/vault/access-tokens/{token_id}")
|
|
103
|
+
self._raise_for_status(response)
|
|
104
|
+
|
|
105
|
+
# ----- error handling -----------------------------------------------
|
|
106
|
+
|
|
107
|
+
@staticmethod
|
|
108
|
+
def _raise_for_status(response: httpx.Response) -> None:
|
|
109
|
+
if response.is_success:
|
|
110
|
+
return
|
|
111
|
+
error_code: str | None = None
|
|
112
|
+
message: str = response.text
|
|
113
|
+
try:
|
|
114
|
+
body = response.json()
|
|
115
|
+
detail = body.get("detail") if isinstance(body, dict) else None
|
|
116
|
+
if isinstance(detail, dict):
|
|
117
|
+
error_code = detail.get("errorCode")
|
|
118
|
+
messages = detail.get("errorMessages") or {}
|
|
119
|
+
# Prefer en-US; fall back to whatever's there.
|
|
120
|
+
message = (
|
|
121
|
+
messages.get("en-US")
|
|
122
|
+
or next(iter(messages.values()), None)
|
|
123
|
+
or message
|
|
124
|
+
)
|
|
125
|
+
except ValueError:
|
|
126
|
+
pass
|
|
127
|
+
raise MoviteraError(
|
|
128
|
+
message,
|
|
129
|
+
status_code=response.status_code,
|
|
130
|
+
error_code=error_code,
|
|
131
|
+
)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
_DEFAULT_API_URL = "https://api.movitera.com"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class Settings:
|
|
11
|
+
api_url: str
|
|
12
|
+
default_team_id: str | None
|
|
13
|
+
inline_token: str | None
|
|
14
|
+
|
|
15
|
+
@classmethod
|
|
16
|
+
def from_env(cls) -> Settings:
|
|
17
|
+
return cls(
|
|
18
|
+
api_url=os.environ.get("MOVITERA_API_URL", _DEFAULT_API_URL).rstrip("/"),
|
|
19
|
+
default_team_id=os.environ.get("MOVITERA_TEAM") or None,
|
|
20
|
+
# MOVITERA_TOKEN lets CI runners skip the keyring entirely.
|
|
21
|
+
inline_token=os.environ.get("MOVITERA_TOKEN") or None,
|
|
22
|
+
)
|