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.
@@ -0,0 +1,188 @@
1
+ """Operation ids from the public OpenAPI document.
2
+
3
+ Generated by sdks/generate.py. Do not edit by hand.
4
+ """
5
+
6
+ from typing import Literal
7
+
8
+ OperationId = Literal[
9
+ "list_reseller_accounts",
10
+ "get_reseller_info",
11
+ "esim_status_per_account",
12
+ "list_sponsors",
13
+ "list_steering_lists",
14
+ "get_subscriber",
15
+ "list_subscribers",
16
+ "create_subscriber",
17
+ "get_sim_provider_status",
18
+ "change_sim_status",
19
+ "get_subscriber_location",
20
+ "hlr_get_bitrate",
21
+ "hlr_set_bitrate",
22
+ "list_subscriber_packages",
23
+ "assign_package",
24
+ "clean_all_packages",
25
+ "list_package_templates",
26
+ "create_package_template",
27
+ "list_location_zones",
28
+ "create_location_zone",
29
+ "list_detailed_location_zones",
30
+ "list_destination_prefixes",
31
+ "subscriber_usage",
32
+ "subscriber_network_events",
33
+ "subscriber_active_period",
34
+ "get_tariff",
35
+ "list_network_profiles",
36
+ "modify_subscriber_balance",
37
+ "modify_subscriber_status",
38
+ "modify_subscriber_contact_info",
39
+ "set_subscriber_traffic_restrictions",
40
+ "modify_subscriber_steering_list",
41
+ "move_subscriber_range_to_account",
42
+ "assign_recurring_package",
43
+ "modify_package_limits",
44
+ "modify_package_expiry",
45
+ "modify_package_status",
46
+ "stop_resume_recurring_package",
47
+ "modify_account_balance",
48
+ "delete_subscriber_package",
49
+ "modify_template_core",
50
+ "modify_template_recurring",
51
+ "modify_template_throttling",
52
+ "send_sms",
53
+ "fleet_health",
54
+ "churn_risk",
55
+ "audit_network_coverage",
56
+ "diagnose_subscriber",
57
+ "detect_usage_anomalies",
58
+ "optimize_package",
59
+ "marketing_intelligence",
60
+ "high_cost_subscribers",
61
+ "high_cost_subscribers_method_guard",
62
+ "detect_country_entry",
63
+ "list_intelligence_schedules",
64
+ "create_intelligence_schedule",
65
+ "delete_intelligence_schedule",
66
+ "run_intelligence_schedule",
67
+ "get_intelligence_schedule_report",
68
+ "get_intelligence_share",
69
+ "operator_billing_connect_begin",
70
+ "operator_billing_connect_oauth_begin",
71
+ "operator_billing_connect_oauth_complete",
72
+ "operator_billing_connect_status",
73
+ "operator_billing_connect_dashboard_link",
74
+ "operator_billing_list_payouts",
75
+ "operator_billing_create_manual_payout",
76
+ "operator_billing_get_payout_schedule",
77
+ "operator_billing_update_payout_schedule",
78
+ "operator_billing_list_fees",
79
+ "operator_billing_list_charges",
80
+ "operator_billing_get_balance",
81
+ "operator_billing_list_refunds",
82
+ "operator_billing_create_refund",
83
+ "operator_billing_list_disputes",
84
+ "operator_billing_submit_dispute_evidence",
85
+ "operator_billing_list_fraud_reviews",
86
+ "operator_billing_approve_fraud_review",
87
+ "operator_billing_decline_fraud_review",
88
+ "operator_billing_list_value_lists",
89
+ "operator_billing_add_value_list_item",
90
+ "affiliates_me_get",
91
+ "affiliates_me_create",
92
+ "affiliates_me_commissions_list",
93
+ "affiliates_link",
94
+ "operator_identity_status",
95
+ "operator_identity_create_session",
96
+ "operator_identity_update_session",
97
+ "operator_list_promotions",
98
+ "operator_create_promotion",
99
+ "signup_start",
100
+ "signup_verify",
101
+ "create_api_key",
102
+ "get_onboarding_state",
103
+ "onboarding_save_org",
104
+ "onboarding_connect_ocs_token",
105
+ "onboarding_start_managed_account",
106
+ "onboarding_save_plan",
107
+ "onboarding_skip_plan",
108
+ "onboarding_test_esim",
109
+ "onboarding_complete",
110
+ "operator_billing_stripe_webhook",
111
+ "receive_bridge4ip_webhook",
112
+ "affect_subscriber_phone_number",
113
+ "modify_subscriber_mobile_plan",
114
+ "modify_subscriber_voip_plan",
115
+ "push_steering_to_subscriber",
116
+ "modify_subscriber_package_active_period",
117
+ "get_subscriber_location_by_cell_id",
118
+ "reset_subscriber_gz_counter",
119
+ "list_destination_lists",
120
+ "list_subscriber_voip_tariff",
121
+ "list_voip_tariff_rule",
122
+ "change_network_profile_of_location_zone",
123
+ "get_network_default",
124
+ "set_network_default",
125
+ "plan_network_default",
126
+ "carrier_webhook_config",
127
+ "ai_chat",
128
+ "ai_insights",
129
+ "ai_quota",
130
+ "list_invoice_periods",
131
+ "get_billing_config",
132
+ "update_billing_config",
133
+ "stripe_connect_begin",
134
+ "stripe_connect_status",
135
+ "list_invoices",
136
+ "create_invoice",
137
+ "get_invoice",
138
+ "update_invoice",
139
+ "void_invoice",
140
+ "get_invoice_document",
141
+ "list_connectivity_plans",
142
+ "list_connectivity_products",
143
+ "create_connectivity_product",
144
+ "get_connectivity_product",
145
+ "list_connectivity_intents",
146
+ "create_connectivity_intent",
147
+ "get_connectivity_intent",
148
+ "confirm_connectivity_intent",
149
+ "create_connectivity_webhook_endpoint",
150
+ "list_connectivity_webhook_endpoints",
151
+ "delete_connectivity_webhook_endpoint",
152
+ "list_connectivity_events",
153
+ "get_connectivity_event",
154
+ "create_connectivity_usage_record",
155
+ "list_connectivity_usage_records",
156
+ "retry_connectivity_event",
157
+ "issue_esim_from_inventory",
158
+ "rollback_esim_issuance",
159
+ "subscriber_country_history",
160
+ "get_subscriber_notes",
161
+ "add_subscriber_note",
162
+ "put_subscriber_notes",
163
+ "observe_connectivity_event",
164
+ "ingest_connectivity_usage",
165
+ "ingest_cdr_events",
166
+ "aggregate_cdr",
167
+ "list_storefront_templates",
168
+ "list_storefront_buyer_orders",
169
+ "get_storefront_order_usage",
170
+ "get_storefront_order_top_up_options",
171
+ "cancel_storefront_top_up_checkout",
172
+ "cancel_storefront_order",
173
+ "get_storefront_order",
174
+ "rollback_storefront_order",
175
+ "get_hosted_storefront",
176
+ "get_hosted_storefront_plan_readiness",
177
+ "list_storefronts",
178
+ "create_storefront",
179
+ "list_storefront_orders",
180
+ "get_storefront",
181
+ "update_storefront",
182
+ "upsert_storefront_plan",
183
+ "update_storefront_plan",
184
+ "publish_storefront",
185
+ "plan_storefront_publish",
186
+ "unpublish_storefront",
187
+ "health_check",
188
+ ]
carrier/operations.py ADDED
@@ -0,0 +1,192 @@
1
+ """Generic caller for every operation id in the public OpenAPI document."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from collections.abc import Mapping
7
+ from urllib.parse import quote
8
+
9
+ from carrier.catalog import HEADER_FIELDS, Operation, operation
10
+ from carrier.errors import CarrierError
11
+ from carrier.http import Http
12
+ from carrier.idempotency import is_issuance, require_id, require_idempotency_key, require_issuance_key
13
+
14
+ _PATH_PARAM = re.compile(r"\{([^{}]+)\}")
15
+ _IDEMPOTENCY_HEADERS = {"Idempotency-Key", "X-Idempotency-Key"}
16
+
17
+
18
+ def call_operation(
19
+ http: Http,
20
+ operation_id: str,
21
+ *,
22
+ path: Mapping[str, object] | None = None,
23
+ query: Mapping[str, object] | None = None,
24
+ headers: Mapping[str, object] | None = None,
25
+ body: object | None = None,
26
+ idempotency_key: str | None = None,
27
+ ) -> object:
28
+ """Call one operation by its OpenAPI operationId."""
29
+ spec = operation(operation_id)
30
+ path_values = _named(path, "path")
31
+ query_values = _named(query, "query")
32
+ header_values = _named(headers, "header")
33
+ _reject_unknown("path", path_values, spec["pathParams"])
34
+ _reject_unknown("query", query_values, spec["queryParams"])
35
+ _reject_unknown("header", header_values, spec["headerParams"])
36
+ _require_present("Path", path_values, spec["pathParams"])
37
+ _apply_idempotency(spec, header_values, idempotency_key)
38
+ _require_headers(header_values, spec)
39
+ for name in list(header_values):
40
+ if name in _IDEMPOTENCY_HEADERS:
41
+ if is_issuance(spec["method"], spec["path"]):
42
+ header_values[name] = require_issuance_key(header_values[name])
43
+ else:
44
+ header_values[name] = require_idempotency_key(str(header_values[name]))
45
+ if spec["bodyRequired"] and body is None:
46
+ raise CarrierError(400, "missing_body", "This operation requires a JSON body.")
47
+ if body is not None and not spec["body"]:
48
+ raise CarrierError(400, "invalid_body", "This operation does not take a JSON body.")
49
+ idempotency = header_values.pop("Idempotency-Key", None)
50
+ encoded_query = {key: _query_value(value) for key, value in query_values.items()}
51
+ return http.request(
52
+ spec["method"],
53
+ _fill_path(spec["path"], path_values),
54
+ body=body,
55
+ query=encoded_query or None,
56
+ headers={key: str(value) for key, value in header_values.items()} or None,
57
+ idempotency_key=str(idempotency) if idempotency is not None else None,
58
+ )
59
+
60
+
61
+ def input_schema(spec: Operation) -> dict[str, object]:
62
+ """JSON schema for the arguments accepted by one operation."""
63
+ properties: dict[str, object] = {}
64
+ required: list[str] = []
65
+ for param in spec["pathParams"]:
66
+ properties[param["name"]] = {"type": "string", "description": f"Path {param['name']}"}
67
+ if param["required"]:
68
+ required.append(param["name"])
69
+ for param in spec["queryParams"]:
70
+ properties[param["name"]] = {"type": "string", "description": f"Query {param['name']}"}
71
+ if param["required"]:
72
+ required.append(param["name"])
73
+ for param in spec["headerParams"]:
74
+ field = HEADER_FIELDS.get(param["name"], param["name"])
75
+ schema: dict[str, object] = {"type": "string", "description": param["name"]}
76
+ if is_issuance(spec["method"], spec["path"]) and field == "idempotency_key":
77
+ schema.update({"minLength": 1, "maxLength": 200, "pattern": "^[A-Za-z0-9][A-Za-z0-9_.:-]{0,199}$"})
78
+ properties[field] = schema
79
+ if param["required"]:
80
+ required.append(field)
81
+ if spec["body"]:
82
+ properties["body"] = {"type": "object", "additionalProperties": True, "description": "JSON request body"}
83
+ if spec["bodyRequired"]:
84
+ required.append("body")
85
+ return {
86
+ "type": "object",
87
+ "properties": properties,
88
+ "required": required,
89
+ "additionalProperties": False,
90
+ }
91
+
92
+
93
+ def arguments_to_call(spec: Operation, arguments: Mapping[str, object]) -> dict[str, object]:
94
+ """Split a flat tool argument object into call_operation keyword arguments."""
95
+ path: dict[str, object] = {}
96
+ query: dict[str, object] = {}
97
+ headers: dict[str, object] = {}
98
+ body = arguments.get("body") if "body" in arguments else None
99
+ known = {param["name"] for param in spec["pathParams"]}
100
+ known.update(param["name"] for param in spec["queryParams"])
101
+ header_names = {HEADER_FIELDS.get(param["name"], param["name"]): param["name"] for param in spec["headerParams"]}
102
+ known.update(header_names)
103
+ if spec["body"]:
104
+ known.add("body")
105
+ unknown = [key for key in arguments if key not in known]
106
+ if unknown:
107
+ raise CarrierError(400, "unknown_parameter", f"Unknown argument {unknown[0]}.")
108
+ for param in spec["pathParams"]:
109
+ if param["name"] in arguments:
110
+ path[param["name"]] = arguments[param["name"]]
111
+ for param in spec["queryParams"]:
112
+ if param["name"] in arguments:
113
+ query[param["name"]] = arguments[param["name"]]
114
+ for field, header in header_names.items():
115
+ if field in arguments and field != "idempotency_key":
116
+ headers[header] = arguments[field]
117
+ call: dict[str, object] = {"path": path, "query": query, "headers": headers}
118
+ if "body" in arguments:
119
+ call["body"] = body
120
+ if "idempotency_key" in arguments:
121
+ call["idempotency_key"] = arguments["idempotency_key"]
122
+ return call
123
+
124
+
125
+ def _named(values: Mapping[str, object] | None, label: str) -> dict[str, object]:
126
+ if values is None:
127
+ return {}
128
+ named: dict[str, object] = {}
129
+ for key, value in values.items():
130
+ if not isinstance(key, str) or key.strip() == "":
131
+ raise CarrierError(400, "invalid_parameter", f"{label} parameter names must be strings.")
132
+ named[key] = value
133
+ return named
134
+
135
+
136
+ def _reject_unknown(label: str, values: Mapping[str, object], params: list[dict[str, object]]) -> None:
137
+ allowed = {str(param["name"]) for param in params}
138
+ for name in values:
139
+ if name not in allowed:
140
+ raise CarrierError(400, "unknown_parameter", f"Unknown {label} parameter {name}.")
141
+
142
+
143
+ def _require_present(label: str, values: Mapping[str, object], params: list[dict[str, object]]) -> None:
144
+ for param in params:
145
+ if param["required"] and param["name"] not in values:
146
+ raise CarrierError(400, "missing_parameter", f"{label} parameter {param['name']} is required.")
147
+
148
+
149
+ def _apply_idempotency(spec: Operation, headers: dict[str, object], idempotency_key: str | None) -> None:
150
+ if idempotency_key is None:
151
+ return
152
+ names = {param["name"] for param in spec["headerParams"]}
153
+ if "Idempotency-Key" in names:
154
+ headers["Idempotency-Key"] = idempotency_key
155
+ return
156
+ if "X-Idempotency-Key" in names:
157
+ headers["X-Idempotency-Key"] = idempotency_key
158
+ return
159
+ raise CarrierError(400, "unknown_parameter", "This operation does not take an idempotency key.")
160
+
161
+
162
+ def _require_headers(values: Mapping[str, object], spec: Operation) -> None:
163
+ for param in spec["headerParams"]:
164
+ if not param["required"] or param["name"] in values:
165
+ continue
166
+ if param["name"] == "X-Idempotency-Key" and "Idempotency-Key" in values:
167
+ continue
168
+ field = HEADER_FIELDS.get(param["name"], param["name"])
169
+ raise CarrierError(400, "missing_parameter", f"{field} is required.")
170
+
171
+
172
+ def _fill_path(template: str, values: Mapping[str, object]) -> str:
173
+ def replace(match: re.Match[str]) -> str:
174
+ name = match.group(1)
175
+ if name not in values:
176
+ raise CarrierError(400, "missing_parameter", f"Path parameter {name} is required.")
177
+ return quote(require_id(str(values[name]), name), safe="")
178
+
179
+ filled = _PATH_PARAM.sub(replace, template)
180
+ if "{" in filled or "}" in filled:
181
+ raise CarrierError(400, "invalid_path", "Operation path could not be filled.")
182
+ return filled
183
+
184
+
185
+ def _query_value(value: object) -> str:
186
+ if isinstance(value, bool):
187
+ return "true" if value else "false"
188
+ if isinstance(value, str):
189
+ return value
190
+ if isinstance(value, int):
191
+ return str(value)
192
+ raise CarrierError(400, "invalid_parameter", "Query values must be strings, integers, or booleans.")
carrier/py.typed ADDED
File without changes
carrier/records.py ADDED
@@ -0,0 +1,117 @@
1
+ """Webhook, event, and usage objects."""
2
+
3
+ from typing import Any
4
+
5
+ from carrier.models import _bool, _int, _invalid, _number, _str
6
+
7
+
8
+ class WebhookEndpoint:
9
+ """A registered HTTPS endpoint. The signing secret is not included."""
10
+
11
+ def __init__(self, id: str, object: str, url: str, status: str, created: int) -> None:
12
+ self.id = id
13
+ self.object = object
14
+ self.url = url
15
+ self.status = status
16
+ self.created = created
17
+
18
+ @classmethod
19
+ def from_dict(cls, data: dict[str, Any]) -> "WebhookEndpoint":
20
+ return cls(
21
+ id=_str(data, "id"),
22
+ object=_str(data, "object"),
23
+ url=_str(data, "url"),
24
+ status=_str(data, "status"),
25
+ created=_int(data, "created"),
26
+ )
27
+
28
+
29
+ class WebhookEndpointCreated(WebhookEndpoint):
30
+ """Create response. `secret` is returned once."""
31
+
32
+ def __init__(self, id: str, object: str, url: str, status: str, created: int, secret: str) -> None:
33
+ super().__init__(id, object, url, status, created)
34
+ self.secret = secret
35
+
36
+ @classmethod
37
+ def from_dict(cls, data: dict[str, Any]) -> "WebhookEndpointCreated":
38
+ base = WebhookEndpoint.from_dict(data)
39
+ return cls(base.id, base.object, base.url, base.status, base.created, _str(data, "secret"))
40
+
41
+
42
+ class DeletedWebhookEndpoint:
43
+ """Result of removing a webhook endpoint."""
44
+
45
+ def __init__(self, id: str, object: str, deleted: bool) -> None:
46
+ self.id = id
47
+ self.object = object
48
+ self.deleted = deleted
49
+
50
+ @classmethod
51
+ def from_dict(cls, data: dict[str, Any]) -> "DeletedWebhookEndpoint":
52
+ return cls(id=_str(data, "id"), object=_str(data, "object"), deleted=_bool(data, "deleted"))
53
+
54
+
55
+ class ConnectivityEvent:
56
+ """A connectivity event delivered to webhook endpoints."""
57
+
58
+ def __init__(
59
+ self,
60
+ id: str,
61
+ object: str,
62
+ type: str,
63
+ livemode: bool,
64
+ created: int,
65
+ data: dict[str, Any],
66
+ ) -> None:
67
+ self.id = id
68
+ self.object = object
69
+ self.type = type
70
+ self.livemode = livemode
71
+ self.created = created
72
+ self.data = data
73
+
74
+ @classmethod
75
+ def from_dict(cls, data: dict[str, Any]) -> "ConnectivityEvent":
76
+ payload = data.get("data")
77
+ if not isinstance(payload, dict):
78
+ raise _invalid("data")
79
+ return cls(
80
+ id=_str(data, "id"),
81
+ object=_str(data, "object"),
82
+ type=_str(data, "type"),
83
+ livemode=_bool(data, "livemode"),
84
+ created=_int(data, "created"),
85
+ data=payload,
86
+ )
87
+
88
+
89
+ class ConnectivityUsageRecord:
90
+ """A usage line recorded against one intent."""
91
+
92
+ def __init__(
93
+ self,
94
+ id: str,
95
+ object: str,
96
+ intent: str,
97
+ quantity_mb: float,
98
+ livemode: bool,
99
+ created: int,
100
+ ) -> None:
101
+ self.id = id
102
+ self.object = object
103
+ self.intent = intent
104
+ self.quantity_mb = quantity_mb
105
+ self.livemode = livemode
106
+ self.created = created
107
+
108
+ @classmethod
109
+ def from_dict(cls, data: dict[str, Any]) -> "ConnectivityUsageRecord":
110
+ return cls(
111
+ id=_str(data, "id"),
112
+ object=_str(data, "object"),
113
+ intent=_str(data, "intent"),
114
+ quantity_mb=_number(data, "quantity_mb"),
115
+ livemode=_bool(data, "livemode"),
116
+ created=_int(data, "created"),
117
+ )