qurl-python 0.2.1__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.
layerv_qurl/types.py ADDED
@@ -0,0 +1,690 @@
1
+ """Type definitions for the qURL API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from datetime import datetime
7
+ from typing import Any, Literal, TypedDict
8
+
9
+ #: Valid resource-level status values. Resources only have two states per
10
+ #: the OpenAPI spec (``QurlData.status``) and the server code. Accepts known
11
+ #: values for IDE autocomplete, plus ``str`` for forward compatibility.
12
+ QURLStatus = Literal["active", "revoked"] | str
13
+
14
+ #: Valid per-token status values. Wider than :data:`QURLStatus` because
15
+ #: individual access tokens can additionally be ``consumed`` (one-time use)
16
+ #: or ``expired``, per the OpenAPI spec (``QurlSummary.status``).
17
+ TokenStatus = Literal["active", "consumed", "expired", "revoked"] | str
18
+
19
+ #: Valid subscription plan values. Matches the ``QuotaData.plan`` enum in
20
+ #: the OpenAPI spec (``[free, growth, enterprise]``). Accepts arbitrary
21
+ #: strings so the API can add new plans without a breaking SDK change.
22
+ QuotaPlan = Literal["free", "growth", "enterprise"] | str
23
+
24
+ #: Valid resource type values. Public callers primarily use ``url`` and
25
+ #: ``tunnel``; the API may also surface integration-owned ``transit`` rows.
26
+ ResourceType = Literal["url", "tunnel", "transit"] | str
27
+
28
+ WebhookEventType = (
29
+ Literal[
30
+ "qurl.created",
31
+ "qurl.expired",
32
+ "qurl.revoked",
33
+ "qurl.updated",
34
+ "resource.closed",
35
+ "qurl.accessed",
36
+ "qurl.access_denied",
37
+ "qurl.token_exhausted",
38
+ "quota.warning",
39
+ "quota.exceeded",
40
+ "token.minted",
41
+ "token.expired",
42
+ "domain.verified",
43
+ "domain.failed",
44
+ "domain.deleted",
45
+ ]
46
+ | str
47
+ )
48
+
49
+
50
+ def _parse_dt(s: str | None) -> datetime | None:
51
+ """Parse an ISO 8601 datetime string, handling Z suffix for Python 3.10 compat."""
52
+ if s is None:
53
+ return None
54
+ if s.endswith("Z"):
55
+ s = s[:-1] + "+00:00"
56
+ return datetime.fromisoformat(s)
57
+
58
+
59
+ @dataclass
60
+ class AIAgentPolicy:
61
+ """Structured policy for controlling AI agent access."""
62
+
63
+ block_all: bool | None = None
64
+ deny_categories: list[str] | None = None
65
+ allow_categories: list[str] | None = None
66
+
67
+
68
+ @dataclass
69
+ class AccessPolicy:
70
+ """Access control policy for a qURL."""
71
+
72
+ ip_allowlist: list[str] | None = None
73
+ ip_denylist: list[str] | None = None
74
+ geo_allowlist: list[str] | None = None
75
+ geo_denylist: list[str] | None = None
76
+ user_agent_allow_regex: str | None = None
77
+ user_agent_deny_regex: str | None = None
78
+ ai_agent_policy: AIAgentPolicy | None = None
79
+
80
+
81
+ @dataclass
82
+ class AccessToken:
83
+ """An individual access token within a qURL.
84
+
85
+ ``status`` uses the wider :data:`TokenStatus` alias — tokens can be
86
+ ``active``/``consumed``/``expired``/``revoked`` (per ``QurlSummary.status``
87
+ in the spec), while resources are only ``active``/``revoked``.
88
+ """
89
+
90
+ qurl_id: str
91
+ status: TokenStatus
92
+ one_time_use: bool = False
93
+ max_sessions: int = 0
94
+ session_duration: int = 0
95
+ use_count: int = 0
96
+ label: str | None = None
97
+ qurl_site: str | None = None
98
+ access_policy: AccessPolicy | None = None
99
+ created_at: datetime | None = None
100
+ expires_at: datetime | None = None
101
+
102
+
103
+ @dataclass
104
+ class QURL:
105
+ """A qURL resource as returned by the API."""
106
+
107
+ resource_id: str
108
+ target_url: str | None
109
+ status: QURLStatus
110
+ created_at: datetime | None = None
111
+ expires_at: datetime | None = None
112
+ description: str | None = None
113
+ tags: list[str] = field(default_factory=list)
114
+ qurl_site: str | None = None
115
+ custom_domain: str | None = None
116
+ slug: str | None = None
117
+ qurl_count: int | None = None
118
+ access_tokens: list[AccessToken] | None = None
119
+
120
+
121
+ @dataclass
122
+ class CreateOutput:
123
+ """Response from creating a qURL.
124
+
125
+ ``resource_id`` identifies the resource container (grouped by target URL).
126
+ ``qurl_id`` identifies the specific access token created (``q_`` prefix).
127
+ Multiple qURLs for the same target URL share one ``resource_id``.
128
+ """
129
+
130
+ resource_id: str
131
+ qurl_link: str
132
+ qurl_site: str
133
+ expires_at: datetime | None = None
134
+ qurl_id: str | None = None
135
+ label: str | None = None
136
+ branded_domain: str | None = None
137
+ resource_type: ResourceType | None = None
138
+
139
+
140
+ @dataclass
141
+ class MintOutput:
142
+ """Response from minting an access link."""
143
+
144
+ qurl_link: str
145
+ qurl_id: str | None = None
146
+ expires_at: datetime | None = None
147
+ branded_domain: str | None = None
148
+ resource_type: ResourceType | None = None
149
+
150
+
151
+ @dataclass
152
+ class AccessGrant:
153
+ """Details of the network access that was granted."""
154
+
155
+ expires_in: int
156
+ granted_at: datetime | None = None
157
+ src_ip: str = ""
158
+
159
+
160
+ @dataclass
161
+ class ResolveOutput:
162
+ """Response from headless resolution."""
163
+
164
+ target_url: str | None
165
+ resource_id: str
166
+ access_grant: AccessGrant | None = None
167
+
168
+
169
+ @dataclass
170
+ class ListOutput:
171
+ """Response from listing qURLs."""
172
+
173
+ qurls: list[QURL] = field(default_factory=list)
174
+ next_cursor: str | None = None
175
+ has_more: bool = False
176
+
177
+
178
+ @dataclass
179
+ class RateLimits:
180
+ """Rate limit configuration."""
181
+
182
+ create_per_minute: int = 0
183
+ create_per_hour: int = 0
184
+ list_per_minute: int = 0
185
+ resolve_per_minute: int = 0
186
+ max_active_qurls: int = 0
187
+ max_tokens_per_qurl: int = 0
188
+ max_expiry_seconds: int = 0
189
+
190
+
191
+ @dataclass
192
+ class Usage:
193
+ """Usage statistics."""
194
+
195
+ qurls_created: int = 0
196
+ active_qurls: int = 0
197
+ # Changed from float=0.0 — callers must None-check before arithmetic.
198
+ active_qurls_percent: float | None = None
199
+ total_accesses: int = 0
200
+
201
+
202
+ @dataclass
203
+ class Quota:
204
+ """Quota and usage information.
205
+
206
+ ``plan`` is typed via :data:`QuotaPlan` (Literal enum + ``str``
207
+ escape hatch for forward compat). Defaults to the sentinel
208
+ ``"unknown"`` — only hit by tests/bootstrap paths, since the
209
+ ``/v1/quota`` endpoint always returns a populated plan string.
210
+ """
211
+
212
+ plan: QuotaPlan = "unknown"
213
+ period_start: datetime | None = None
214
+ period_end: datetime | None = None
215
+ rate_limits: RateLimits | None = None
216
+ usage: Usage | None = None
217
+
218
+
219
+ @dataclass
220
+ class BatchItemError:
221
+ """Error details for a failed batch item."""
222
+
223
+ code: str = ""
224
+ message: str = ""
225
+
226
+
227
+ @dataclass
228
+ class BatchItemResult:
229
+ """Result for a single item in a batch create."""
230
+
231
+ index: int = 0
232
+ success: bool = False
233
+ resource_id: str | None = None
234
+ qurl_link: str | None = None
235
+ qurl_site: str | None = None
236
+ branded_domain: str | None = None
237
+ expires_at: datetime | None = None
238
+ error: BatchItemError | None = None
239
+
240
+
241
+ @dataclass
242
+ class BatchCreateOutput:
243
+ """Response from batch creating qURLs."""
244
+
245
+ succeeded: int = 0
246
+ failed: int = 0
247
+ results: list[BatchItemResult] = field(default_factory=list)
248
+
249
+
250
+ @dataclass
251
+ class Resource:
252
+ """Resource container returned by the resource-management API."""
253
+
254
+ resource_id: str
255
+ status: QURLStatus
256
+ resource_type: ResourceType | None = None
257
+ target_url: str | None = None
258
+ knock_resource_id: str | None = None
259
+ description: str | None = None
260
+ tags: list[str] = field(default_factory=list)
261
+ custom_domain: str | None = None
262
+ alias: str | None = None
263
+ slug: str | None = None
264
+ preserve_host: bool = False
265
+ session_duration_cap: int | None = None
266
+ qurl_count: int | None = None
267
+ created_at: datetime | None = None
268
+ expires_at: datetime | None = None
269
+ tombstoned_at: datetime | None = None
270
+
271
+
272
+ @dataclass
273
+ class ResourceDetail:
274
+ """Detailed resource response with a bounded qURL token preview."""
275
+
276
+ resource: Resource
277
+ qurls: list[AccessToken] = field(default_factory=list)
278
+
279
+
280
+ @dataclass
281
+ class ResourceListOutput:
282
+ """Response from listing resources."""
283
+
284
+ resources: list[Resource] = field(default_factory=list)
285
+ next_cursor: str | None = None
286
+ has_more: bool = False
287
+
288
+
289
+ @dataclass
290
+ class Session:
291
+ """Active access session for a resource."""
292
+
293
+ session_id: str
294
+ qurl_id: str | None = None
295
+ src_ip: str | None = None
296
+ user_agent: str | None = None
297
+ created_at: datetime | None = None
298
+ last_seen_at: datetime | None = None
299
+
300
+
301
+ @dataclass
302
+ class SessionListOutput:
303
+ """Response from listing active sessions."""
304
+
305
+ sessions: list[Session] = field(default_factory=list)
306
+
307
+
308
+ @dataclass
309
+ class SessionTerminateOutput:
310
+ """Response from terminating all active sessions on a resource."""
311
+
312
+ terminated: int = 0
313
+
314
+
315
+ @dataclass
316
+ class DNSRecord:
317
+ """DNS record required for custom-domain setup."""
318
+
319
+ type: str = ""
320
+ name: str = ""
321
+ value: str = ""
322
+ verified: bool = False
323
+
324
+
325
+ @dataclass
326
+ class Domain:
327
+ """Custom-domain registration state."""
328
+
329
+ domain: str
330
+ status: str
331
+ # Verification tokens are public DNS values, so repr keeps them visible.
332
+ verification_token: str | None = None
333
+ token_expires_at: datetime | None = None
334
+ acme_cname_target: str | None = None
335
+ created_at: datetime | None = None
336
+ verified_at: datetime | None = None
337
+ activated_at: datetime | None = None
338
+ ready_for_qurls: bool = False
339
+ dns_records: list[DNSRecord] = field(default_factory=list)
340
+
341
+
342
+ @dataclass
343
+ class DomainListOutput:
344
+ """Response from listing custom domains."""
345
+
346
+ domains: list[Domain] = field(default_factory=list)
347
+ next_cursor: str | None = None
348
+ has_more: bool = False
349
+
350
+
351
+ @dataclass
352
+ class CheckDetail:
353
+ """DNS verification check detail."""
354
+
355
+ verified: bool
356
+ error: str | None = None
357
+ found: str | None = None
358
+
359
+
360
+ @dataclass
361
+ class DomainVerifyOutput:
362
+ """Response from triggering custom-domain verification."""
363
+
364
+ domain: str
365
+ status: str
366
+ checks: dict[str, CheckDetail] = field(default_factory=dict)
367
+
368
+
369
+ @dataclass
370
+ class Webhook:
371
+ """Webhook subscription."""
372
+
373
+ webhook_id: str
374
+ url: str
375
+ events: list[WebhookEventType]
376
+ owner_id: str | None = None
377
+ status: str | None = None
378
+ description: str | None = None
379
+ created_at: datetime | None = None
380
+ updated_at: datetime | None = None
381
+ failure_count: int = 0
382
+ last_delivery_success: bool | None = None
383
+ last_delivery_time: int | None = None
384
+ secret: str | None = field(default=None, repr=False)
385
+
386
+
387
+ @dataclass
388
+ class WebhookListOutput:
389
+ """Response from listing webhooks."""
390
+
391
+ webhooks: list[Webhook] = field(default_factory=list)
392
+ next_cursor: str | None = None
393
+ has_more: bool = False
394
+
395
+
396
+ @dataclass
397
+ class WebhookDelivery:
398
+ """Webhook delivery attempt."""
399
+
400
+ delivery_id: str
401
+ webhook_id: str | None = None
402
+ event_type: WebhookEventType | None = None
403
+ status: str | None = None
404
+ response_code: int | None = None
405
+ response_body: str | None = None
406
+ error_message: str | None = None
407
+ duration_ms: int | None = None
408
+ retry_count: int = 0
409
+ created_at: datetime | None = None
410
+ completed_at: datetime | None = None
411
+
412
+
413
+ @dataclass
414
+ class WebhookDeliveryListOutput:
415
+ """Response from listing webhook deliveries."""
416
+
417
+ deliveries: list[WebhookDelivery] = field(default_factory=list)
418
+ next_cursor: str | None = None
419
+ has_more: bool = False
420
+
421
+
422
+ @dataclass
423
+ class WebhookEventTypeInfo:
424
+ """Webhook event catalog entry."""
425
+
426
+ type: WebhookEventType
427
+ category: str | None = None
428
+ description: str | None = None
429
+
430
+
431
+ @dataclass
432
+ class WebhookEventTypesOutput:
433
+ """Response from listing supported webhook event types."""
434
+
435
+ events: list[WebhookEventTypeInfo] = field(default_factory=list)
436
+
437
+
438
+ @dataclass
439
+ class APIKey:
440
+ """API key metadata.
441
+
442
+ ``api_key`` is populated only on create responses.
443
+ """
444
+
445
+ key_id: str
446
+ key_prefix: str
447
+ name: str
448
+ scopes: list[str] = field(default_factory=list)
449
+ status: str | None = None
450
+ created_at: datetime | None = None
451
+ updated_at: datetime | None = None
452
+ last_used_at: datetime | None = None
453
+ expires_at: datetime | None = None
454
+ purpose: str | None = None
455
+ tunnel_slug: str | None = None
456
+ api_key: str | None = field(default=None, repr=False)
457
+
458
+
459
+ @dataclass
460
+ class APIKeyListOutput:
461
+ """Response from listing API keys."""
462
+
463
+ api_keys: list[APIKey] = field(default_factory=list)
464
+ next_cursor: str | None = None
465
+ has_more: bool = False
466
+
467
+
468
+ @dataclass
469
+ class RedeemAccessCodeOutput:
470
+ """Response from redeeming a public access code."""
471
+
472
+ redirect_url: str
473
+
474
+
475
+ @dataclass
476
+ class AccessCode:
477
+ """Access-code metadata.
478
+
479
+ ``code`` is populated only on create responses.
480
+ """
481
+
482
+ access_code_id: str
483
+ resource_id: str
484
+ name: str | None = None
485
+ status: str | None = None
486
+ max_uses: int = 0
487
+ use_count: int = 0
488
+ created_at: datetime | None = None
489
+ expires_at: datetime | None = None
490
+ code: str | None = field(default=None, repr=False)
491
+
492
+
493
+ @dataclass
494
+ class AccessCodeListOutput:
495
+ """Response from listing access codes."""
496
+
497
+ access_codes: list[AccessCode] = field(default_factory=list)
498
+ next_cursor: str | None = None
499
+ has_more: bool = False
500
+
501
+
502
+ @dataclass
503
+ class UsageCostEstimate:
504
+ """Estimated usage cost for the current billing period."""
505
+
506
+ currency: str
507
+ amount_cents: int
508
+ description: str
509
+
510
+
511
+ @dataclass
512
+ class CurrentPeriodUsage:
513
+ """Current billing-period usage summary."""
514
+
515
+ tier: QuotaPlan
516
+ period_start: datetime | None
517
+ period_end: datetime | None
518
+ qurls_created: int
519
+ active_qurls: int
520
+ cost_estimate: UsageCostEstimate | None = None
521
+
522
+
523
+ @dataclass
524
+ class UsageDailyEntry:
525
+ """Daily qURL creation count."""
526
+
527
+ date: str
528
+ qurls_created: int
529
+
530
+
531
+ @dataclass
532
+ class DailyUsage:
533
+ """Daily usage breakdown for the current billing period."""
534
+
535
+ tier: QuotaPlan
536
+ period_start: datetime | None
537
+ period_end: datetime | None
538
+ daily: list[UsageDailyEntry] = field(default_factory=list)
539
+
540
+
541
+ @dataclass
542
+ class Customer:
543
+ """Customer profile and billing settings."""
544
+
545
+ tier: QuotaPlan
546
+ spending_cap_cents: int
547
+ current_period_usage_count: int
548
+ frozen: bool
549
+ frozen_reason: str | None = None
550
+
551
+
552
+ @dataclass
553
+ class CheckoutSession:
554
+ """Stripe checkout session URL."""
555
+
556
+ url: str
557
+
558
+
559
+ @dataclass
560
+ class PortalSession:
561
+ """Stripe billing portal session URL."""
562
+
563
+ url: str
564
+
565
+
566
+ @dataclass
567
+ class Invoice:
568
+ """Billing invoice summary."""
569
+
570
+ id: str
571
+ amount_cents: int
572
+ status: str
573
+ created_at: datetime | None = None
574
+ pdf_url: str | None = None
575
+
576
+
577
+ @dataclass
578
+ class InvoiceListOutput:
579
+ """Response from listing billing invoices."""
580
+
581
+ invoices: list[Invoice] = field(default_factory=list)
582
+ next_cursor: str | None = None
583
+ has_more: bool = False
584
+
585
+
586
+ @dataclass
587
+ class ConnectorInstallationStats:
588
+ """Connector installation activity counters."""
589
+
590
+ resources: int = 0
591
+ qurls: int = 0
592
+ accesses_24h: int = 0
593
+ accesses_7d: int = 0
594
+ errors_24h: int = 0
595
+
596
+
597
+ @dataclass
598
+ class ConnectorInstallationCapabilities:
599
+ """Connector installation capability flags."""
600
+
601
+ configure: bool = False
602
+ disconnect: bool = False
603
+ reauth: bool = False
604
+ view_activity: bool = False
605
+
606
+
607
+ @dataclass
608
+ class ConnectorInstallation:
609
+ """Normalized connector installation summary."""
610
+
611
+ installation_id: str
612
+ plugin_id: str
613
+ label: str
614
+ subject_kind: str
615
+ subject_display_name: str
616
+ status: str
617
+ installed_at: datetime | None = None
618
+ last_activity_at: datetime | None = None
619
+ stats: ConnectorInstallationStats | None = None
620
+ capabilities: ConnectorInstallationCapabilities | None = None
621
+
622
+
623
+ @dataclass
624
+ class ConnectorInstallationListOutput:
625
+ """Response from listing connector installations."""
626
+
627
+ installations: list[ConnectorInstallation] = field(default_factory=list)
628
+ next_cursor: str | None = None
629
+ has_more: bool = False
630
+
631
+
632
+ @dataclass
633
+ class NHPServerPeerInfo:
634
+ """NHP server peer info returned to connector agents."""
635
+
636
+ public_key_b64: str
637
+ host: str
638
+ port: int
639
+ expire_time: int
640
+
641
+
642
+ @dataclass
643
+ class AgentBootstrapOutput:
644
+ """Response from connector agent bootstrap."""
645
+
646
+ agent_id: str
647
+ registered_at: datetime | None
648
+ nhp_server_peer: NHPServerPeerInfo
649
+
650
+
651
+ # ---- batch_create input shape -------------------------------------------
652
+ # TypedDicts for :meth:`QURLClient.batch_create` items. Split into a
653
+ # required-fields base class and an `total=False` subclass so callers on
654
+ # Python 3.10 don't need ``typing.Required`` (added in 3.11) or a
655
+ # ``typing_extensions`` dependency. Fields mirror the corresponding
656
+ # keyword arguments on :meth:`QURLClient.create` one-for-one; the
657
+ # single-create endpoint and the batch endpoint share the same
658
+ # ``CreateQurlRequest`` schema in the OpenAPI spec.
659
+ #
660
+ # The runtime behavior is unchanged — ``batch_create`` still iterates
661
+ # items as plain dicts and passes them through ``build_body`` /
662
+ # ``_serialize_value``. The TypedDict is purely for IDE autocomplete and
663
+ # static type checking.
664
+
665
+
666
+ class _BatchCreateItemRequired(TypedDict):
667
+ target_url: str
668
+
669
+
670
+ class BatchCreateItem(_BatchCreateItemRequired, total=False):
671
+ """Input shape for a single item in :meth:`QURLClient.batch_create`.
672
+
673
+ ``target_url`` is required; every other field is optional and mirrors
674
+ the corresponding keyword argument on :meth:`QURLClient.create`.
675
+
676
+ ``access_policy`` accepts either an :class:`AccessPolicy` dataclass
677
+ (recommended for type safety and IDE autocomplete) or a plain
678
+ ``dict[str, Any]`` (for callers who prefer dicts or are working from
679
+ dynamic config). Both forms are converted to the same JSON body at
680
+ request time via ``_serialize_value``.
681
+ """
682
+
683
+ type: str
684
+ expires_in: str
685
+ label: str
686
+ one_time_use: bool
687
+ max_sessions: int
688
+ session_duration: str
689
+ access_policy: AccessPolicy | dict[str, Any]
690
+ custom_domain: str