carrier-python 0.1.15__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.
Files changed (31) hide show
  1. carrier_python-0.1.15/PKG-INFO +87 -0
  2. carrier_python-0.1.15/README.md +72 -0
  3. carrier_python-0.1.15/pyproject.toml +28 -0
  4. carrier_python-0.1.15/setup.cfg +4 -0
  5. carrier_python-0.1.15/src/carrier/__init__.py +52 -0
  6. carrier_python-0.1.15/src/carrier/adapters.py +121 -0
  7. carrier_python-0.1.15/src/carrier/catalog.py +108 -0
  8. carrier_python-0.1.15/src/carrier/cli.py +70 -0
  9. carrier_python-0.1.15/src/carrier/client.py +63 -0
  10. carrier_python-0.1.15/src/carrier/data/cli.json +3032 -0
  11. carrier_python-0.1.15/src/carrier/data/operations.json +3042 -0
  12. carrier_python-0.1.15/src/carrier/errors.py +51 -0
  13. carrier_python-0.1.15/src/carrier/http.py +135 -0
  14. carrier_python-0.1.15/src/carrier/idempotency.py +46 -0
  15. carrier_python-0.1.15/src/carrier/mcp.py +237 -0
  16. carrier_python-0.1.15/src/carrier/models.py +213 -0
  17. carrier_python-0.1.15/src/carrier/operation_ids.py +188 -0
  18. carrier_python-0.1.15/src/carrier/operations.py +192 -0
  19. carrier_python-0.1.15/src/carrier/py.typed +0 -0
  20. carrier_python-0.1.15/src/carrier/records.py +117 -0
  21. carrier_python-0.1.15/src/carrier/resources.py +201 -0
  22. carrier_python-0.1.15/src/carrier/signature.py +57 -0
  23. carrier_python-0.1.15/src/carrier/version.py +3 -0
  24. carrier_python-0.1.15/src/carrier_python.egg-info/PKG-INFO +87 -0
  25. carrier_python-0.1.15/src/carrier_python.egg-info/SOURCES.txt +29 -0
  26. carrier_python-0.1.15/src/carrier_python.egg-info/dependency_links.txt +1 -0
  27. carrier_python-0.1.15/src/carrier_python.egg-info/top_level.txt +1 -0
  28. carrier_python-0.1.15/tests/test_client.py +177 -0
  29. carrier_python-0.1.15/tests/test_issuance.py +184 -0
  30. carrier_python-0.1.15/tests/test_signature.py +78 -0
  31. carrier_python-0.1.15/tests/test_surface.py +215 -0
@@ -0,0 +1,87 @@
1
+ Metadata-Version: 2.4
2
+ Name: carrier-python
3
+ Version: 0.1.15
4
+ Summary: Python client for the Carrier API, MCP server, and CLI.
5
+ Author: Lifecycle Innovations Limited
6
+ License: MIT
7
+ Project-URL: Homepage, https://carrier.llc/sdks/
8
+ Project-URL: Documentation, https://carrier.llc/docs/sdks/
9
+ Keywords: carrier,esim,connectivity
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: OS Independent
13
+ Requires-Python: >=3.11
14
+ Description-Content-Type: text/markdown
15
+
16
+ # carrier-python
17
+
18
+ Python client for the Carrier API, the hosted MCP server, and the `carrier` CLI. `intents.create` posts to `/v1/connectivity/intents`. `call` uses an OpenAPI operation id. `Mcp` speaks Streamable HTTP. `Cli` runs the binary with an argument list. `rest_tools`, `mcp_tools`, and `cli_tools` return plain dicts for LangGraph or an OpenAI-style tool loop. The price on a product is catalogue data. Neither an intent nor a product debits a wallet.
19
+
20
+ ```bash
21
+ pip install carrier-python
22
+ ```
23
+
24
+ ```python
25
+ import os
26
+ from carrier import Carrier
27
+
28
+ carrier = Carrier(os.environ["CARRIER_API_KEY"])
29
+ intent = carrier.intents.create(
30
+ plan="test_global_1gb",
31
+ metadata={"order": "123"},
32
+ idempotency_key="order_123",
33
+ )
34
+ ```
35
+
36
+ `test_global_1gb` is a practice plan. The call succeeds, `livemode` is false, and the network is not used. The same idempotency key and body return the original intent.
37
+
38
+ A waiting intent takes a plan later:
39
+
40
+ ```python
41
+ carrier.intents.confirm(intent.id, plan="test_global_1gb")
42
+ ```
43
+
44
+ Webhook deliveries use `Carrier-Signature: t=<unix>,v1=<hex>` over `{timestamp}.{body}`. The endpoint secret (`whsec_…`) is returned once, on create.
45
+
46
+ ```python
47
+ from carrier import verify_webhook_signature
48
+
49
+ ok = verify_webhook_signature(raw_body, header, secret)
50
+ ```
51
+
52
+ `CarrierError.status`, `.code`, and `.type` match the API error envelope. A blank API key raises before a request is sent.
53
+
54
+ ## Issuance safety (0.1.11)
55
+
56
+ For `intents.create` and `call("issue_esim_from_inventory", ...)`, supply a stable business key. Issuance keys must match `[A-Za-z0-9][A-Za-z0-9_.:-]{0,199}`. They are never trimmed, coerced, or replaced. Invalid keys fail before transport; nonissuance operations retain their existing key rules.
57
+
58
+ Transport loss, response-read failure, or response-decode failure after an issuance call raises `CarrierError` with `code == "outcome_unknown"`, the unchanged `.idempotency_key`, and `.retryable is False`. The SDK does not retry dispatch or mint a new key. Reconcile the original operation using that key; do not automatically reissue. A complete valid API error keeps its server status, code, message, and type. This is not a provider exactly-once guarantee. Custom transports must not retry issuance internally.
59
+
60
+ Every public operation:
61
+
62
+ ```python
63
+ accounts = carrier.call("list_reseller_accounts")
64
+ issued = carrier.call(
65
+ "create_connectivity_intent",
66
+ body={"plan": "test_global_1gb"},
67
+ idempotency_key="order_123",
68
+ )
69
+ ```
70
+
71
+ `security` on each catalog entry is `bearer`, `public`, `internal`, or `storefront`. A customer API key is `bearer`. The two internal routes do not accept that key.
72
+
73
+ ```python
74
+ from carrier import Mcp, Cli, rest_tools
75
+
76
+ session = Mcp(os.environ["ESIMVAULT_API_TOKEN"]) # ?TOKEN= on https://mcp.carrier.llc/mcp
77
+ tools = session.list_tools()
78
+ session.call_tool("list_subscribers", {"limit": 5})
79
+
80
+ cli = Cli()
81
+ cli.run(["subscribers", "list", "--limit", "5"])
82
+ agent_tools = rest_tools(carrier)
83
+ ```
84
+
85
+ `Mcp(..., auth="bearer")` sends an OAuth access token or a Clerk organization API key. `Cli` never invokes a shell. Pass `strict=False` only when you intentionally run a command that is not in the catalog.
86
+
87
+ Python 3.11 or newer. The package uses the standard library only. `pip install carrier-python` installs it from PyPI.
@@ -0,0 +1,72 @@
1
+ # carrier-python
2
+
3
+ Python client for the Carrier API, the hosted MCP server, and the `carrier` CLI. `intents.create` posts to `/v1/connectivity/intents`. `call` uses an OpenAPI operation id. `Mcp` speaks Streamable HTTP. `Cli` runs the binary with an argument list. `rest_tools`, `mcp_tools`, and `cli_tools` return plain dicts for LangGraph or an OpenAI-style tool loop. The price on a product is catalogue data. Neither an intent nor a product debits a wallet.
4
+
5
+ ```bash
6
+ pip install carrier-python
7
+ ```
8
+
9
+ ```python
10
+ import os
11
+ from carrier import Carrier
12
+
13
+ carrier = Carrier(os.environ["CARRIER_API_KEY"])
14
+ intent = carrier.intents.create(
15
+ plan="test_global_1gb",
16
+ metadata={"order": "123"},
17
+ idempotency_key="order_123",
18
+ )
19
+ ```
20
+
21
+ `test_global_1gb` is a practice plan. The call succeeds, `livemode` is false, and the network is not used. The same idempotency key and body return the original intent.
22
+
23
+ A waiting intent takes a plan later:
24
+
25
+ ```python
26
+ carrier.intents.confirm(intent.id, plan="test_global_1gb")
27
+ ```
28
+
29
+ Webhook deliveries use `Carrier-Signature: t=<unix>,v1=<hex>` over `{timestamp}.{body}`. The endpoint secret (`whsec_…`) is returned once, on create.
30
+
31
+ ```python
32
+ from carrier import verify_webhook_signature
33
+
34
+ ok = verify_webhook_signature(raw_body, header, secret)
35
+ ```
36
+
37
+ `CarrierError.status`, `.code`, and `.type` match the API error envelope. A blank API key raises before a request is sent.
38
+
39
+ ## Issuance safety (0.1.11)
40
+
41
+ For `intents.create` and `call("issue_esim_from_inventory", ...)`, supply a stable business key. Issuance keys must match `[A-Za-z0-9][A-Za-z0-9_.:-]{0,199}`. They are never trimmed, coerced, or replaced. Invalid keys fail before transport; nonissuance operations retain their existing key rules.
42
+
43
+ Transport loss, response-read failure, or response-decode failure after an issuance call raises `CarrierError` with `code == "outcome_unknown"`, the unchanged `.idempotency_key`, and `.retryable is False`. The SDK does not retry dispatch or mint a new key. Reconcile the original operation using that key; do not automatically reissue. A complete valid API error keeps its server status, code, message, and type. This is not a provider exactly-once guarantee. Custom transports must not retry issuance internally.
44
+
45
+ Every public operation:
46
+
47
+ ```python
48
+ accounts = carrier.call("list_reseller_accounts")
49
+ issued = carrier.call(
50
+ "create_connectivity_intent",
51
+ body={"plan": "test_global_1gb"},
52
+ idempotency_key="order_123",
53
+ )
54
+ ```
55
+
56
+ `security` on each catalog entry is `bearer`, `public`, `internal`, or `storefront`. A customer API key is `bearer`. The two internal routes do not accept that key.
57
+
58
+ ```python
59
+ from carrier import Mcp, Cli, rest_tools
60
+
61
+ session = Mcp(os.environ["ESIMVAULT_API_TOKEN"]) # ?TOKEN= on https://mcp.carrier.llc/mcp
62
+ tools = session.list_tools()
63
+ session.call_tool("list_subscribers", {"limit": 5})
64
+
65
+ cli = Cli()
66
+ cli.run(["subscribers", "list", "--limit", "5"])
67
+ agent_tools = rest_tools(carrier)
68
+ ```
69
+
70
+ `Mcp(..., auth="bearer")` sends an OAuth access token or a Clerk organization API key. `Cli` never invokes a shell. Pass `strict=False` only when you intentionally run a command that is not in the catalog.
71
+
72
+ Python 3.11 or newer. The package uses the standard library only. `pip install carrier-python` installs it from PyPI.
@@ -0,0 +1,28 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "carrier-python"
7
+ version = "0.1.15"
8
+ description = "Python client for the Carrier API, MCP server, and CLI."
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Lifecycle Innovations Limited" }]
13
+ keywords = ["carrier", "esim", "connectivity"]
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Operating System :: OS Independent",
18
+ ]
19
+
20
+ [project.urls]
21
+ Homepage = "https://carrier.llc/sdks/"
22
+ Documentation = "https://carrier.llc/docs/sdks/"
23
+
24
+ [tool.setuptools.packages.find]
25
+ where = ["src"]
26
+
27
+ [tool.setuptools.package-data]
28
+ carrier = ["py.typed", "data/*.json"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,52 @@
1
+ """Python client for the Carrier API, MCP server, and CLI."""
2
+
3
+ from carrier.adapters import cli_tools, mcp_tools, rest_tools
4
+ from carrier.catalog import cli_commands, operations
5
+ from carrier.cli import Cli, CliResult
6
+ from carrier.client import Carrier
7
+ from carrier.errors import CarrierError
8
+ from carrier.mcp import DEFAULT_MCP_URL, Mcp
9
+ from carrier.operation_ids import OperationId
10
+ from carrier.models import (
11
+ ConnectivityEsim,
12
+ ConnectivityIntent,
13
+ ConnectivityPlan,
14
+ IntentError,
15
+ PlanPrice,
16
+ )
17
+ from carrier.records import (
18
+ ConnectivityEvent,
19
+ ConnectivityUsageRecord,
20
+ DeletedWebhookEndpoint,
21
+ WebhookEndpoint,
22
+ WebhookEndpointCreated,
23
+ )
24
+ from carrier.signature import verify_webhook_signature
25
+ from carrier.version import __version__
26
+
27
+ __all__ = [
28
+ "DEFAULT_MCP_URL",
29
+ "Carrier",
30
+ "CarrierError",
31
+ "Cli",
32
+ "CliResult",
33
+ "Mcp",
34
+ "OperationId",
35
+ "ConnectivityEsim",
36
+ "ConnectivityEvent",
37
+ "ConnectivityIntent",
38
+ "ConnectivityPlan",
39
+ "ConnectivityUsageRecord",
40
+ "DeletedWebhookEndpoint",
41
+ "IntentError",
42
+ "PlanPrice",
43
+ "WebhookEndpoint",
44
+ "WebhookEndpointCreated",
45
+ "__version__",
46
+ "cli_commands",
47
+ "cli_tools",
48
+ "mcp_tools",
49
+ "operations",
50
+ "rest_tools",
51
+ "verify_webhook_signature",
52
+ ]
@@ -0,0 +1,121 @@
1
+ """Dependency-free tool definitions for LangGraph, OpenAI, and the Vercel AI SDK.
2
+
3
+ Each tool is a plain dict with `name`, `description`, `inputSchema`, and
4
+ `execute`. Pass `inputSchema` to an AI SDK `jsonSchema()` helper, or use the
5
+ dict as a LangGraph / OpenAI function tool. Nothing in this module imports
6
+ those libraries.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from collections.abc import Mapping
12
+ from typing import TypedDict
13
+
14
+ from carrier.catalog import Operation, cli_commands, operations
15
+ from carrier.cli import Cli, CliResult
16
+ from carrier.client import Carrier
17
+ from carrier.errors import CarrierError
18
+ from carrier.mcp import Mcp
19
+ from carrier.operations import arguments_to_call, input_schema
20
+
21
+
22
+ class Tool(TypedDict):
23
+ """One callable tool definition."""
24
+
25
+ name: str
26
+ description: str
27
+ inputSchema: dict[str, object]
28
+ execute: object
29
+
30
+
31
+ def rest_tools(client: Carrier) -> list[Tool]:
32
+ """One tool per OpenAPI operation. `execute` calls `client.call`."""
33
+ tools: list[Tool] = []
34
+ for spec in operations():
35
+
36
+ def execute(arguments: Mapping[str, object], spec: Operation = spec) -> object:
37
+ prepared = arguments_to_call(spec, arguments)
38
+ return client.call(spec["id"], **_call_kwargs(prepared))
39
+
40
+ tools.append(
41
+ {
42
+ "name": spec["id"],
43
+ "description": f"{spec['summary']} ({spec['method']} {spec['path']}, {spec['security']})",
44
+ "inputSchema": input_schema(spec),
45
+ "execute": execute,
46
+ }
47
+ )
48
+ return tools
49
+
50
+
51
+ def mcp_tools(session: Mcp) -> list[Tool]:
52
+ """One tool per MCP tool advertised by `session.list_tools()`."""
53
+ tools: list[Tool] = []
54
+ for advertised in session.list_tools():
55
+ if not isinstance(advertised, dict) or not isinstance(advertised.get("name"), str):
56
+ continue
57
+ name = advertised["name"]
58
+ description = advertised.get("description")
59
+ schema = advertised.get("inputSchema")
60
+ input_schema_value = schema if isinstance(schema, dict) else {"type": "object", "properties": {}}
61
+
62
+ def execute(arguments: Mapping[str, object], tool_name: str = name) -> dict[str, object]:
63
+ return session.call_tool(tool_name, arguments)
64
+
65
+ tools.append(
66
+ {
67
+ "name": name,
68
+ "description": description if isinstance(description, str) and description else name,
69
+ "inputSchema": input_schema_value,
70
+ "execute": execute,
71
+ }
72
+ )
73
+ return tools
74
+
75
+
76
+ def cli_tools(cli: Cli) -> list[Tool]:
77
+ """One tool per catalogued CLI command. Extra flags go in `args`."""
78
+ tools: list[Tool] = []
79
+ for command in cli_commands():
80
+ tokens = command["invocation"].split()[1:]
81
+ option_text = ", ".join(option["flags"] for option in command["options"]) or "no flags"
82
+ description = f"{command['summary']} Flags: {option_text}."
83
+
84
+ def execute(arguments: Mapping[str, object], tokens: list[str] = tokens) -> CliResult:
85
+ extra = arguments.get("args", [])
86
+ if extra is None:
87
+ extra = []
88
+ if not isinstance(extra, list) or not all(isinstance(item, str) for item in extra):
89
+ raise CarrierError(400, "invalid_argument", "CLI tool args must be a list of strings.")
90
+ return cli.run([*tokens, *extra])
91
+
92
+ tools.append(
93
+ {
94
+ "name": "cli_" + "_".join(tokens),
95
+ "description": description,
96
+ "inputSchema": {
97
+ "type": "object",
98
+ "properties": {
99
+ "args": {
100
+ "type": "array",
101
+ "items": {"type": "string"},
102
+ "description": "Flags and values after the command name.",
103
+ }
104
+ },
105
+ "additionalProperties": False,
106
+ },
107
+ "execute": execute,
108
+ }
109
+ )
110
+ return tools
111
+
112
+
113
+ def _call_kwargs(prepared: Mapping[str, object]) -> dict[str, object]:
114
+ kwargs: dict[str, object] = {}
115
+ for key in ("path", "query", "headers", "body", "idempotency_key"):
116
+ if key in prepared:
117
+ kwargs[key] = prepared[key]
118
+ return kwargs
119
+
120
+
121
+ __all__ = ["Tool", "cli_tools", "mcp_tools", "rest_tools"]
@@ -0,0 +1,108 @@
1
+ """Operation and CLI catalogs generated from the public contract."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from functools import lru_cache
7
+ from importlib.resources import files
8
+ from typing import TypedDict, cast
9
+
10
+ from carrier.errors import CarrierError
11
+ from carrier.operation_ids import OperationId
12
+
13
+ HEADER_FIELDS = {
14
+ "Idempotency-Key": "idempotency_key",
15
+ "X-Idempotency-Key": "idempotency_key",
16
+ "X-Confirm-Token": "confirm_token",
17
+ "stripe-signature": "stripe_signature",
18
+ }
19
+
20
+
21
+ class Param(TypedDict):
22
+ """One path, query, or header parameter."""
23
+
24
+ name: str
25
+ required: bool
26
+
27
+
28
+ class Operation(TypedDict):
29
+ """One OpenAPI operation the generic caller can invoke."""
30
+
31
+ id: OperationId
32
+ method: str
33
+ path: str
34
+ tag: str
35
+ summary: str
36
+ security: str
37
+ body: bool
38
+ bodyRequired: bool
39
+ pathParams: list[Param]
40
+ queryParams: list[Param]
41
+ headerParams: list[Param]
42
+
43
+
44
+ class CliOption(TypedDict):
45
+ """One flag on a carrier CLI command."""
46
+
47
+ flags: str
48
+ description: str
49
+ required: bool
50
+
51
+
52
+ class CliCommand(TypedDict):
53
+ """One command from the published CLI catalog."""
54
+
55
+ invocation: str
56
+ domain: str
57
+ summary: str
58
+ write: bool
59
+ transport: str
60
+ tool: str | None
61
+ restMethod: str | None
62
+ restPath: str | None
63
+ options: list[CliOption]
64
+
65
+
66
+ @lru_cache(maxsize=1)
67
+ def operations() -> tuple[Operation, ...]:
68
+ """Every operation in the public OpenAPI document, in spec order."""
69
+ return tuple(cast(list[Operation], _load("operations.json")))
70
+
71
+
72
+ @lru_cache(maxsize=1)
73
+ def cli_commands() -> tuple[CliCommand, ...]:
74
+ """Every command in the published CLI catalog."""
75
+ return tuple(cast(list[CliCommand], _load("cli.json")))
76
+
77
+
78
+ def operation(operation_id: str) -> Operation:
79
+ """Return one operation, or raise when the id is not in the catalog."""
80
+ found = _operation_index().get(operation_id)
81
+ if found is None:
82
+ raise CarrierError(400, "unknown_operation", f"Unknown operation {operation_id}.")
83
+ return found
84
+
85
+
86
+ def command_for_args(args: list[str]) -> CliCommand | None:
87
+ """Match the longest catalog command that is a prefix of args."""
88
+ best: CliCommand | None = None
89
+ best_len = -1
90
+ for command in cli_commands():
91
+ tokens = command["invocation"].split()[1:]
92
+ if args[: len(tokens)] == tokens and len(tokens) > best_len:
93
+ best = command
94
+ best_len = len(tokens)
95
+ return best
96
+
97
+
98
+ @lru_cache(maxsize=1)
99
+ def _operation_index() -> dict[str, Operation]:
100
+ return {item["id"]: item for item in operations()}
101
+
102
+
103
+ def _load(name: str) -> list[object]:
104
+ raw = files("carrier").joinpath("data", name).read_text(encoding="utf-8")
105
+ parsed = json.loads(raw)
106
+ if not isinstance(parsed, list):
107
+ raise CarrierError(500, "invalid_catalog", f"{name} is not a list.")
108
+ return cast(list[object], parsed)
@@ -0,0 +1,70 @@
1
+ """Runner for the carrier CLI. Arguments are passed as a list, never through a shell."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import subprocess
6
+ from collections.abc import Callable, Sequence
7
+ from dataclasses import dataclass
8
+
9
+ from carrier.catalog import CliCommand, cli_commands, command_for_args
10
+ from carrier.errors import CarrierError
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class CliResult:
15
+ """Stdout and stderr from one carrier invocation."""
16
+
17
+ code: int
18
+ stdout: str
19
+ stderr: str
20
+
21
+
22
+ Runner = Callable[[str, list[str]], CliResult]
23
+
24
+
25
+ class Cli:
26
+ """Run catalogued `carrier` commands."""
27
+
28
+ def __init__(self, *, binary: str = "carrier", runner: Runner | None = None) -> None:
29
+ if binary.strip() == "" or "\x00" in binary:
30
+ raise CarrierError(400, "invalid_cli", "The carrier binary path is not valid.")
31
+ self.binary = binary
32
+ self._runner = runner if runner is not None else _subprocess_runner
33
+
34
+ def commands(self) -> tuple[CliCommand, ...]:
35
+ """Every command in the published catalog."""
36
+ return cli_commands()
37
+
38
+ def describe(self, invocation: str) -> CliCommand:
39
+ """Return one catalog entry, such as `carrier subscribers list`."""
40
+ for command in cli_commands():
41
+ if command["invocation"] == invocation:
42
+ return command
43
+ raise CarrierError(400, "unknown_command", f"Unknown command {invocation}.")
44
+
45
+ def run(self, args: Sequence[str], *, strict: bool = True) -> CliResult:
46
+ """Run `carrier` with args. Strict mode rejects commands missing from the catalog."""
47
+ argv = _validate_args(args)
48
+ if strict and command_for_args(argv) is None:
49
+ joined = " ".join(argv) if argv else "(empty)"
50
+ raise CarrierError(400, "unknown_command", f"Unknown command carrier {joined}.")
51
+ return self._runner(self.binary, argv)
52
+
53
+
54
+ def _validate_args(args: Sequence[str]) -> list[str]:
55
+ argv: list[str] = []
56
+ for arg in args:
57
+ if not isinstance(arg, str) or "\x00" in arg:
58
+ raise CarrierError(400, "invalid_argument", "CLI arguments must be strings without NUL bytes.")
59
+ argv.append(arg)
60
+ return argv
61
+
62
+
63
+ def _subprocess_runner(binary: str, args: list[str]) -> CliResult:
64
+ try:
65
+ completed = subprocess.run([binary, *args], capture_output=True, text=True, check=False)
66
+ except FileNotFoundError as error:
67
+ raise CarrierError(0, "cli_not_found", f"Could not run {binary}.") from error
68
+ except OSError as error:
69
+ raise CarrierError(0, "cli_not_found", str(error) or f"Could not run {binary}.") from error
70
+ return CliResult(completed.returncode, completed.stdout, completed.stderr)
@@ -0,0 +1,63 @@
1
+ """Typed client for the Carrier connectivity API."""
2
+
3
+ from collections.abc import Mapping
4
+
5
+ from carrier.errors import CarrierError
6
+ from carrier.http import DEFAULT_BASE_URL, Http, Transport, urllib_transport
7
+ from carrier.operation_ids import OperationId
8
+ from carrier.operations import call_operation
9
+ from carrier.resources import Events, Intents, Plans, Products, UsageRecords, WebhookEndpoints
10
+
11
+
12
+ class Carrier:
13
+ """Client for one Carrier API key.
14
+
15
+ `intents.create` issues one eSIM. `call` reaches every public OpenAPI
16
+ operation id. The price on a product is catalogue data. Neither the intent
17
+ nor the product call debits a wallet.
18
+ """
19
+
20
+ def __init__(
21
+ self,
22
+ api_key: str,
23
+ *,
24
+ base_url: str = DEFAULT_BASE_URL,
25
+ transport: Transport | None = None,
26
+ timeout: float = 30,
27
+ ) -> None:
28
+ if api_key.strip() == "":
29
+ raise CarrierError(400, "invalid_api_key", "A Carrier API key is required.")
30
+ http = Http(
31
+ api_key,
32
+ base_url or DEFAULT_BASE_URL,
33
+ transport if transport is not None else urllib_transport(timeout),
34
+ timeout,
35
+ )
36
+ self._http = http
37
+ self.intents = Intents(http)
38
+ self.products = Products(http)
39
+ self.plans = Plans(http)
40
+ self.webhook_endpoints = WebhookEndpoints(http)
41
+ self.events = Events(http)
42
+ self.usage_records = UsageRecords(http)
43
+
44
+ def call(
45
+ self,
46
+ operation_id: OperationId,
47
+ *,
48
+ path: Mapping[str, object] | None = None,
49
+ query: Mapping[str, object] | None = None,
50
+ headers: Mapping[str, object] | None = None,
51
+ body: object | None = None,
52
+ idempotency_key: str | None = None,
53
+ ) -> object:
54
+ """Call any operation id from the public OpenAPI document."""
55
+ return call_operation(
56
+ self._http,
57
+ operation_id,
58
+ path=path,
59
+ query=query,
60
+ headers=headers,
61
+ body=body,
62
+ idempotency_key=idempotency_key,
63
+ )