carrier-python 0.1.15__py3-none-any.whl
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.
- carrier/__init__.py +52 -0
- carrier/adapters.py +121 -0
- carrier/catalog.py +108 -0
- carrier/cli.py +70 -0
- carrier/client.py +63 -0
- carrier/data/cli.json +3032 -0
- carrier/data/operations.json +3042 -0
- carrier/errors.py +51 -0
- carrier/http.py +135 -0
- carrier/idempotency.py +46 -0
- carrier/mcp.py +237 -0
- carrier/models.py +213 -0
- carrier/operation_ids.py +188 -0
- carrier/operations.py +192 -0
- carrier/py.typed +0 -0
- carrier/records.py +117 -0
- carrier/resources.py +201 -0
- carrier/signature.py +57 -0
- carrier/version.py +3 -0
- carrier_python-0.1.15.dist-info/METADATA +87 -0
- carrier_python-0.1.15.dist-info/RECORD +23 -0
- carrier_python-0.1.15.dist-info/WHEEL +5 -0
- carrier_python-0.1.15.dist-info/top_level.txt +1 -0
carrier/resources.py
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""Resource clients. Each method maps to one connectivity route."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping, Sequence
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from carrier.errors import CarrierError, outcome_unknown
|
|
7
|
+
from carrier.http import Http
|
|
8
|
+
from carrier.idempotency import require_id
|
|
9
|
+
from carrier.models import ConnectivityIntent, ConnectivityPlan, parse_list
|
|
10
|
+
from carrier.records import (
|
|
11
|
+
ConnectivityEvent,
|
|
12
|
+
ConnectivityUsageRecord,
|
|
13
|
+
DeletedWebhookEndpoint,
|
|
14
|
+
WebhookEndpoint,
|
|
15
|
+
WebhookEndpointCreated,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Intents:
|
|
20
|
+
"""Issue, read, and confirm connectivity intents."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, http: Http) -> None:
|
|
23
|
+
self._http = http
|
|
24
|
+
|
|
25
|
+
def create(
|
|
26
|
+
self,
|
|
27
|
+
*,
|
|
28
|
+
plan: str | None = None,
|
|
29
|
+
metadata: Mapping[str, str] | None = None,
|
|
30
|
+
idempotency_key: str,
|
|
31
|
+
) -> ConnectivityIntent:
|
|
32
|
+
"""POST /v1/connectivity/intents. The same key and body return the original intent."""
|
|
33
|
+
body: dict[str, Any] = {}
|
|
34
|
+
if plan is not None:
|
|
35
|
+
body["plan"] = plan
|
|
36
|
+
if metadata is not None:
|
|
37
|
+
body["metadata"] = dict(metadata)
|
|
38
|
+
payload = self._http.request(
|
|
39
|
+
"POST",
|
|
40
|
+
"/v1/connectivity/intents",
|
|
41
|
+
body=body,
|
|
42
|
+
idempotency_key=idempotency_key,
|
|
43
|
+
)
|
|
44
|
+
try:
|
|
45
|
+
return ConnectivityIntent.from_dict(_as_object(payload))
|
|
46
|
+
except CarrierError as error:
|
|
47
|
+
raise outcome_unknown(idempotency_key) from error
|
|
48
|
+
|
|
49
|
+
def retrieve(self, intent_id: str) -> ConnectivityIntent:
|
|
50
|
+
"""GET /v1/connectivity/intents/:id."""
|
|
51
|
+
path = f"/v1/connectivity/intents/{require_id(intent_id, 'intent')}"
|
|
52
|
+
return ConnectivityIntent.from_dict(_as_object(self._http.request("GET", path)))
|
|
53
|
+
|
|
54
|
+
def list(self, *, limit: int | None = None) -> list[ConnectivityIntent]:
|
|
55
|
+
"""GET /v1/connectivity/intents. Newest first. Omit limit for the default page of 20."""
|
|
56
|
+
query: dict[str, str] = {}
|
|
57
|
+
if limit is not None:
|
|
58
|
+
if isinstance(limit, bool) or not isinstance(limit, int) or limit < 1 or limit > 100:
|
|
59
|
+
raise CarrierError(400, "invalid_limit", "limit must be an integer from 1 to 100.")
|
|
60
|
+
query["limit"] = str(limit)
|
|
61
|
+
payload = self._http.request("GET", "/v1/connectivity/intents", query=query or None)
|
|
62
|
+
return parse_list(payload, ConnectivityIntent.from_dict)
|
|
63
|
+
|
|
64
|
+
def confirm(self, intent_id: str, *, plan: str) -> ConnectivityIntent:
|
|
65
|
+
"""POST /v1/connectivity/intents/:id/confirm."""
|
|
66
|
+
path = f"/v1/connectivity/intents/{require_id(intent_id, 'intent')}/confirm"
|
|
67
|
+
payload = self._http.request("POST", path, body={"plan": plan})
|
|
68
|
+
return ConnectivityIntent.from_dict(_as_object(payload))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class Products:
|
|
72
|
+
"""Operator-defined plans. Creating one does not debit a wallet."""
|
|
73
|
+
|
|
74
|
+
def __init__(self, http: Http) -> None:
|
|
75
|
+
self._http = http
|
|
76
|
+
|
|
77
|
+
def create(
|
|
78
|
+
self,
|
|
79
|
+
*,
|
|
80
|
+
name: str,
|
|
81
|
+
data_mb: int,
|
|
82
|
+
validity_days: int,
|
|
83
|
+
countries: Sequence[str],
|
|
84
|
+
price_cents: int,
|
|
85
|
+
currency: str,
|
|
86
|
+
package_template_id: int,
|
|
87
|
+
account_id: int | None = None,
|
|
88
|
+
idempotency_key: str,
|
|
89
|
+
) -> ConnectivityPlan:
|
|
90
|
+
"""POST /v1/connectivity/products. price_cents is catalogue data."""
|
|
91
|
+
body: dict[str, Any] = {
|
|
92
|
+
"name": name,
|
|
93
|
+
"data_mb": data_mb,
|
|
94
|
+
"validity_days": validity_days,
|
|
95
|
+
"countries": list(countries),
|
|
96
|
+
"price_cents": price_cents,
|
|
97
|
+
"currency": currency,
|
|
98
|
+
"package_template_id": package_template_id,
|
|
99
|
+
}
|
|
100
|
+
if account_id is not None:
|
|
101
|
+
body["account_id"] = account_id
|
|
102
|
+
payload = self._http.request(
|
|
103
|
+
"POST",
|
|
104
|
+
"/v1/connectivity/products",
|
|
105
|
+
body=body,
|
|
106
|
+
idempotency_key=idempotency_key,
|
|
107
|
+
)
|
|
108
|
+
return ConnectivityPlan.from_dict(_as_object(payload))
|
|
109
|
+
|
|
110
|
+
def retrieve(self, product_id: str) -> ConnectivityPlan:
|
|
111
|
+
"""GET /v1/connectivity/products/:id."""
|
|
112
|
+
path = f"/v1/connectivity/products/{require_id(product_id, 'product')}"
|
|
113
|
+
return ConnectivityPlan.from_dict(_as_object(self._http.request("GET", path)))
|
|
114
|
+
|
|
115
|
+
def list(self) -> list[ConnectivityPlan]:
|
|
116
|
+
"""GET /v1/connectivity/products. Newest first, up to 100."""
|
|
117
|
+
return parse_list(self._http.request("GET", "/v1/connectivity/products"), ConnectivityPlan.from_dict)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class Plans:
|
|
121
|
+
"""Test plans and live plans this account can issue."""
|
|
122
|
+
|
|
123
|
+
def __init__(self, http: Http) -> None:
|
|
124
|
+
self._http = http
|
|
125
|
+
|
|
126
|
+
def list(self) -> list[ConnectivityPlan]:
|
|
127
|
+
"""GET /v1/connectivity/plans."""
|
|
128
|
+
return parse_list(self._http.request("GET", "/v1/connectivity/plans"), ConnectivityPlan.from_dict)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class WebhookEndpoints:
|
|
132
|
+
"""HTTPS endpoints that receive Carrier-Signature deliveries."""
|
|
133
|
+
|
|
134
|
+
def __init__(self, http: Http) -> None:
|
|
135
|
+
self._http = http
|
|
136
|
+
|
|
137
|
+
def create(self, url: str) -> WebhookEndpointCreated:
|
|
138
|
+
"""POST /v1/connectivity/webhook_endpoints. The secret is on this response only."""
|
|
139
|
+
payload = self._http.request("POST", "/v1/connectivity/webhook_endpoints", body={"url": url})
|
|
140
|
+
return WebhookEndpointCreated.from_dict(_as_object(payload))
|
|
141
|
+
|
|
142
|
+
def list(self) -> list[WebhookEndpoint]:
|
|
143
|
+
"""GET /v1/connectivity/webhook_endpoints. Secrets are omitted."""
|
|
144
|
+
payload = self._http.request("GET", "/v1/connectivity/webhook_endpoints")
|
|
145
|
+
return parse_list(payload, WebhookEndpoint.from_dict)
|
|
146
|
+
|
|
147
|
+
def delete(self, endpoint_id: str) -> DeletedWebhookEndpoint:
|
|
148
|
+
"""DELETE /v1/connectivity/webhook_endpoints/:id."""
|
|
149
|
+
path = f"/v1/connectivity/webhook_endpoints/{require_id(endpoint_id, 'webhook endpoint')}"
|
|
150
|
+
return DeletedWebhookEndpoint.from_dict(_as_object(self._http.request("DELETE", path)))
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class Events:
|
|
154
|
+
"""Connectivity events for this account."""
|
|
155
|
+
|
|
156
|
+
def __init__(self, http: Http) -> None:
|
|
157
|
+
self._http = http
|
|
158
|
+
|
|
159
|
+
def list(self, *, intent: str | None = None) -> list[ConnectivityEvent]:
|
|
160
|
+
"""GET /v1/connectivity/events. Newest first, at most 20."""
|
|
161
|
+
query = None if intent is None else {"intent": require_id(intent, "intent")}
|
|
162
|
+
return parse_list(self._http.request("GET", "/v1/connectivity/events", query=query), ConnectivityEvent.from_dict)
|
|
163
|
+
|
|
164
|
+
def retrieve(self, event_id: str) -> ConnectivityEvent:
|
|
165
|
+
"""GET /v1/connectivity/events/:id."""
|
|
166
|
+
path = f"/v1/connectivity/events/{require_id(event_id, 'event')}"
|
|
167
|
+
return ConnectivityEvent.from_dict(_as_object(self._http.request("GET", path)))
|
|
168
|
+
|
|
169
|
+
def retry(self, event_id: str) -> ConnectivityEvent:
|
|
170
|
+
"""POST /v1/connectivity/events/:id/retry."""
|
|
171
|
+
path = f"/v1/connectivity/events/{require_id(event_id, 'event')}/retry"
|
|
172
|
+
return ConnectivityEvent.from_dict(_as_object(self._http.request("POST", path)))
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class UsageRecords:
|
|
176
|
+
"""Usage lines on a test intent. Live usage is metered by the network."""
|
|
177
|
+
|
|
178
|
+
def __init__(self, http: Http) -> None:
|
|
179
|
+
self._http = http
|
|
180
|
+
|
|
181
|
+
def create(self, *, intent: str, quantity_mb: float, idempotency_key: str) -> ConnectivityUsageRecord:
|
|
182
|
+
"""POST /v1/connectivity/usage_records."""
|
|
183
|
+
payload = self._http.request(
|
|
184
|
+
"POST",
|
|
185
|
+
"/v1/connectivity/usage_records",
|
|
186
|
+
body={"intent": intent, "quantity_mb": quantity_mb},
|
|
187
|
+
idempotency_key=idempotency_key,
|
|
188
|
+
)
|
|
189
|
+
return ConnectivityUsageRecord.from_dict(_as_object(payload))
|
|
190
|
+
|
|
191
|
+
def list(self, intent: str) -> list[ConnectivityUsageRecord]:
|
|
192
|
+
"""GET /v1/connectivity/usage_records?intent=."""
|
|
193
|
+
query = {"intent": require_id(intent, "intent")}
|
|
194
|
+
payload = self._http.request("GET", "/v1/connectivity/usage_records", query=query)
|
|
195
|
+
return parse_list(payload, ConnectivityUsageRecord.from_dict)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _as_object(payload: object) -> dict[str, Any]:
|
|
199
|
+
if not isinstance(payload, dict):
|
|
200
|
+
raise CarrierError(0, "invalid_response", "Carrier response is not an object.")
|
|
201
|
+
return payload
|
carrier/signature.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Webhook signature check. Same header the API sends as Carrier-Signature."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import hmac
|
|
5
|
+
import time
|
|
6
|
+
from typing import Final
|
|
7
|
+
|
|
8
|
+
DEFAULT_TOLERANCE_SECONDS: Final = 300
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def verify_webhook_signature(
|
|
12
|
+
body: str,
|
|
13
|
+
signature: str,
|
|
14
|
+
secret: str,
|
|
15
|
+
*,
|
|
16
|
+
tolerance_seconds: int = DEFAULT_TOLERANCE_SECONDS,
|
|
17
|
+
now_seconds: int | None = None,
|
|
18
|
+
) -> bool:
|
|
19
|
+
"""Return whether `signature` signs `body` under `secret`.
|
|
20
|
+
|
|
21
|
+
The header is `t=<unix>,v1=<hex>` over `{timestamp}.{body}`. A rotation
|
|
22
|
+
can put more than one `v1` value in the header. Any match passes.
|
|
23
|
+
`tolerance_seconds` of 0 turns the replay window off.
|
|
24
|
+
"""
|
|
25
|
+
timestamps: list[str] = []
|
|
26
|
+
candidates: list[str] = []
|
|
27
|
+
for part in signature.split(","):
|
|
28
|
+
if "=" not in part:
|
|
29
|
+
continue
|
|
30
|
+
key, value = part.split("=", 1)
|
|
31
|
+
key = key.strip()
|
|
32
|
+
value = value.strip()
|
|
33
|
+
if value == "":
|
|
34
|
+
continue
|
|
35
|
+
if key == "t":
|
|
36
|
+
timestamps.append(value)
|
|
37
|
+
elif key == "v1":
|
|
38
|
+
candidates.append(value)
|
|
39
|
+
if not timestamps or not candidates:
|
|
40
|
+
return False
|
|
41
|
+
timestamp = timestamps[0]
|
|
42
|
+
if tolerance_seconds < 0:
|
|
43
|
+
return False
|
|
44
|
+
if tolerance_seconds > 0:
|
|
45
|
+
try:
|
|
46
|
+
signed_at = int(timestamp)
|
|
47
|
+
except ValueError:
|
|
48
|
+
return False
|
|
49
|
+
clock = int(time.time()) if now_seconds is None else now_seconds
|
|
50
|
+
if abs(clock - signed_at) > tolerance_seconds:
|
|
51
|
+
return False
|
|
52
|
+
digest = hmac.new(
|
|
53
|
+
secret.encode("utf-8"),
|
|
54
|
+
f"{timestamp}.{body}".encode("utf-8"),
|
|
55
|
+
hashlib.sha256,
|
|
56
|
+
).hexdigest()
|
|
57
|
+
return any(hmac.compare_digest(digest, candidate) for candidate in candidates)
|
carrier/version.py
ADDED
|
@@ -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,23 @@
|
|
|
1
|
+
carrier/__init__.py,sha256=W9FU9skCg7U4MxhH6UKO_Sxw0R-x_i9eng4Hgkq90js,1291
|
|
2
|
+
carrier/adapters.py,sha256=Akxd630boQdyh4fUctI6xpxqiWze7wdrhHgWxTkD5AA,4374
|
|
3
|
+
carrier/catalog.py,sha256=F_7tTFu8vXJ4NuxUgwfQErRGNQCC9AVWqWxi7drK158,2871
|
|
4
|
+
carrier/cli.py,sha256=iFhB-j2ckurNuEK_e2khpjspYCmAjlb0RUNJJrddufs,2680
|
|
5
|
+
carrier/client.py,sha256=tJEsBoyA7oailopi84tndEy7X7nr3tbrZZ3p46tG5Yw,2074
|
|
6
|
+
carrier/errors.py,sha256=X61cqPBsgBF3-bq41DoNzLepabvhVDarpqF0Y7fyIDE,1923
|
|
7
|
+
carrier/http.py,sha256=h8YAZkG5Ouh4893Sww7CmlcnnsYjNjZJmwE96-ZsCjA,5161
|
|
8
|
+
carrier/idempotency.py,sha256=IzapZp2SmcIYJU9G_vfzsgu0n_h55O4Fcc3A45kaNwE,1698
|
|
9
|
+
carrier/mcp.py,sha256=EtIJ71rXf9bfeOGz93hCVtODnkGq57eY0XdXyiffu2k,9475
|
|
10
|
+
carrier/models.py,sha256=0_kKeAnewlpzwcn8K-KVCqxMQhs6oh70W9FlhNKmy1s,6638
|
|
11
|
+
carrier/operation_ids.py,sha256=GSucnszcNxtbkpUIcRth1oHZNnEOed2RMp3eGl6ldUY,5835
|
|
12
|
+
carrier/operations.py,sha256=4Zl3LIV7XtpP1Or-_IyUUjNs-mE59Aftnu4rrTz2OLw,8279
|
|
13
|
+
carrier/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
+
carrier/records.py,sha256=4e4m0AiePdw5axgJiSfHBt2ftf4Szx5sOl5_rkN15Rw,3442
|
|
15
|
+
carrier/resources.py,sha256=mj97lySHMJi12KQVhXlfZ-grAKmf7XkhgBNi_nGegZg,8057
|
|
16
|
+
carrier/signature.py,sha256=MZRp9Vj9heode_plZvXTGhN6P1wos0DEyOvS29l2XtA,1724
|
|
17
|
+
carrier/version.py,sha256=Mw8J1E9E8IbEMhFW_X8i2lF4PHFFzmUP-0N3u4L-1ZQ,108
|
|
18
|
+
carrier/data/cli.json,sha256=omgHkZLsfMwxT8SQg29vJjl43lswtZE0sGUmpR9L_uM,87647
|
|
19
|
+
carrier/data/operations.json,sha256=MXjZcvXUO8W35ir77R_BjidtmWGUex8QmkBfcrRShUk,73707
|
|
20
|
+
carrier_python-0.1.15.dist-info/METADATA,sha256=PJvDHp0kMUeOvf8AR-Kvmtxslo7dqJUjhNg5FyvokR0,3817
|
|
21
|
+
carrier_python-0.1.15.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
22
|
+
carrier_python-0.1.15.dist-info/top_level.txt,sha256=yXdhRVSdu7ru_mUxsikeswMIz0nQubrqn1MQa6EW2fQ,8
|
|
23
|
+
carrier_python-0.1.15.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
carrier
|