kc-sdk-python 5.3.0__tar.gz → 5.3.2__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.
- {kc_sdk_python-5.3.0/kc_sdk_python.egg-info → kc_sdk_python-5.3.2}/PKG-INFO +1 -1
- {kc_sdk_python-5.3.0 → kc_sdk_python-5.3.2}/kc_api.py +140 -36
- {kc_sdk_python-5.3.0 → kc_sdk_python-5.3.2/kc_sdk_python.egg-info}/PKG-INFO +1 -1
- {kc_sdk_python-5.3.0 → kc_sdk_python-5.3.2}/pyproject.toml +1 -1
- {kc_sdk_python-5.3.0 → kc_sdk_python-5.3.2}/LICENSE +0 -0
- {kc_sdk_python-5.3.0 → kc_sdk_python-5.3.2}/README.md +0 -0
- {kc_sdk_python-5.3.0 → kc_sdk_python-5.3.2}/kc_sdk_python.egg-info/SOURCES.txt +0 -0
- {kc_sdk_python-5.3.0 → kc_sdk_python-5.3.2}/kc_sdk_python.egg-info/dependency_links.txt +0 -0
- {kc_sdk_python-5.3.0 → kc_sdk_python-5.3.2}/kc_sdk_python.egg-info/requires.txt +0 -0
- {kc_sdk_python-5.3.0 → kc_sdk_python-5.3.2}/kc_sdk_python.egg-info/top_level.txt +0 -0
- {kc_sdk_python-5.3.0 → kc_sdk_python-5.3.2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kc-sdk-python
|
|
3
|
-
Version: 5.3.
|
|
3
|
+
Version: 5.3.2
|
|
4
4
|
Summary: Kvindo Cloud Python SDK — typed client for managing Kvindo Cloud infrastructure (VMs, S3, Kubernetes, load balancers, VPCs, PostgreSQL) via the REST API
|
|
5
5
|
Author: Kvindo
|
|
6
6
|
License-Expression: MIT
|
|
@@ -313,6 +313,60 @@ class KcResourceGetByLabelsResponse(object):
|
|
|
313
313
|
# outside this set is an unexpected transport/server failure and is raised instead.
|
|
314
314
|
_HANDLED_STATUS_CODES = [200, 400, 401, 403, 409, 422]
|
|
315
315
|
|
|
316
|
+
# Defaults for the SubmitLockBusy/TransactionDeleteLockBusy retry below - overridable per-call
|
|
317
|
+
# for a caller that wants a shorter fail-fast budget (e.g. an interactive script) instead of
|
|
318
|
+
# blocking like an async job worker would.
|
|
319
|
+
_DEFAULT_LOCK_BUSY_RETRY_BUDGET_SECONDS = 1200 # ~20min, matches kc_svc_job.py's existing precedent
|
|
320
|
+
_DEFAULT_LOCK_BUSY_RETRY_DELAY_SECONDS = 30 # fallback when Retry-After is absent/unparseable
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _parse_retry_after_seconds(value) -> float:
|
|
324
|
+
"""Parse an HTTP `Retry-After` header value.
|
|
325
|
+
|
|
326
|
+
This API only ever sends the RFC 9110 delta-seconds form (a plain integer string) -
|
|
327
|
+
see `CloudApiControllerBase.SetRetryAfterIfLockBusy` on the server side. Falls back to
|
|
328
|
+
the default delay on a missing or malformed header rather than raising, since a
|
|
329
|
+
busy-lock retry should never itself crash on a header-parsing edge case.
|
|
330
|
+
"""
|
|
331
|
+
try:
|
|
332
|
+
return max(0.0, float(value))
|
|
333
|
+
except (TypeError, ValueError):
|
|
334
|
+
return _DEFAULT_LOCK_BUSY_RETRY_DELAY_SECONDS
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _retry_while_lock_busy(perform_request, is_busy, retry_budget_seconds):
|
|
338
|
+
"""Call `perform_request()` (returns `(requests.Response, parsed_result)`), retrying
|
|
339
|
+
while `is_busy(parsed_result)` is True, for up to `retry_budget_seconds` of wall-clock
|
|
340
|
+
time (measured end-to-end across the whole loop, including each `perform_request()`
|
|
341
|
+
call's own latency, not just time spent sleeping between attempts - a slow busy request
|
|
342
|
+
itself eats into the budget, which is deliberate: the contract is "give up trying after
|
|
343
|
+
this much real time has passed", not "sleep this much total"). Returns the last
|
|
344
|
+
`(response, parsed_result)` pair - either the first non-busy result, or the last busy
|
|
345
|
+
one once the budget is exhausted (never raises on exhaustion; exhaustion just means the
|
|
346
|
+
caller sees the same busy errorCode/errorMessage they'd have seen immediately before
|
|
347
|
+
this retry existed).
|
|
348
|
+
|
|
349
|
+
Any exception raised by `perform_request()` or `is_busy()` propagates immediately,
|
|
350
|
+
uncaught - a malformed response or a genuine HTTP failure is a different problem than
|
|
351
|
+
lock contention and must never be swallowed by a retry meant only for the latter.
|
|
352
|
+
"""
|
|
353
|
+
deadline = time.monotonic() + retry_budget_seconds
|
|
354
|
+
attempt = 0
|
|
355
|
+
while True:
|
|
356
|
+
attempt += 1
|
|
357
|
+
response, result = perform_request()
|
|
358
|
+
if not is_busy(result):
|
|
359
|
+
return response, result
|
|
360
|
+
remaining = deadline - time.monotonic()
|
|
361
|
+
if remaining <= 0:
|
|
362
|
+
return response, result
|
|
363
|
+
delay = min(_parse_retry_after_seconds(response.headers.get("Retry-After")), remaining)
|
|
364
|
+
logger.warning(
|
|
365
|
+
f"{result.errorCode} (attempt {attempt}), retrying in {delay:.0f}s "
|
|
366
|
+
f"({remaining:.0f}s left in retry budget)"
|
|
367
|
+
)
|
|
368
|
+
time.sleep(delay)
|
|
369
|
+
|
|
316
370
|
|
|
317
371
|
class KcResourceClient:
|
|
318
372
|
"""Client for a single resource type of the Kvindo Cloud API.
|
|
@@ -357,13 +411,23 @@ class KcResourceClient:
|
|
|
357
411
|
"Content-Type": "application/json-patch+json",
|
|
358
412
|
}
|
|
359
413
|
|
|
360
|
-
def delete(
|
|
414
|
+
def delete(
|
|
415
|
+
self,
|
|
416
|
+
id: str,
|
|
417
|
+
wait=False,
|
|
418
|
+
transaction_delete_lock_retry_budget_seconds: float = _DEFAULT_LOCK_BUSY_RETRY_BUDGET_SECONDS,
|
|
419
|
+
) -> KcResourceDeleteResponse:
|
|
361
420
|
"""Delete a resource by id (asynchronous).
|
|
362
421
|
|
|
363
422
|
Args:
|
|
364
423
|
id: id of the resource to delete.
|
|
365
424
|
wait: if True, block (up to 300s) until the delete reconciles via
|
|
366
425
|
`wait_request_satisfied` before returning.
|
|
426
|
+
transaction_delete_lock_retry_budget_seconds: how long to keep retrying a
|
|
427
|
+
TransactionDeleteLockBusy response (only reachable when deleting a
|
|
428
|
+
`transaction` resource specifically) before giving up and returning it
|
|
429
|
+
as-is. Default ~20min. Pass 0 to disable retrying and get the immediate
|
|
430
|
+
fire-once behavior back.
|
|
367
431
|
|
|
368
432
|
Returns:
|
|
369
433
|
KcResourceDeleteResponse with `requestId` to poll, or `errorMessage`/
|
|
@@ -374,24 +438,30 @@ class KcResourceClient:
|
|
|
374
438
|
"""
|
|
375
439
|
url = f"{self.__api_url}/api/v1/{self.__resource_type}/{id}"
|
|
376
440
|
|
|
377
|
-
|
|
441
|
+
def _do_delete():
|
|
442
|
+
response = create_http_client_with_retries(verify_ssl=self.__verify_ssl).delete(url, headers=self.__headers())
|
|
443
|
+
logger.debug(
|
|
444
|
+
f"Got {response.status_code} status code while making request DELETE {url}\nResponse body: {response.text}",
|
|
445
|
+
extra=self.__log_extra,
|
|
446
|
+
)
|
|
447
|
+
if response.status_code not in _HANDLED_STATUS_CODES:
|
|
448
|
+
raise Exception(
|
|
449
|
+
f"Got {response.status_code} status code while making request DELETE {url}\nResponse body: {response.text}"
|
|
450
|
+
)
|
|
451
|
+
return response, KcResourceDeleteResponse.Schema().load(response.json())
|
|
378
452
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
453
|
+
_, result = _retry_while_lock_busy(
|
|
454
|
+
_do_delete,
|
|
455
|
+
lambda r: r.errorCode == KcApiModificationErrorCode.TransactionDeleteLockBusy,
|
|
456
|
+
transaction_delete_lock_retry_budget_seconds,
|
|
382
457
|
)
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
return result
|
|
391
|
-
else:
|
|
392
|
-
raise Exception(
|
|
393
|
-
f"Got {response.status_code} status code while making request DELETE {url}\nResponse body: {response.text}"
|
|
394
|
-
)
|
|
458
|
+
# requestId is only set on a genuinely-accepted delete - a still-busy result after the
|
|
459
|
+
# retry budget is exhausted has requestId=None, and wait_request_satisfied(None, ...)
|
|
460
|
+
# would be a meaningless poll against a nonexistent request. Only wait on a real
|
|
461
|
+
# acceptance.
|
|
462
|
+
if wait and result.requestId is not None:
|
|
463
|
+
self.wait_request_satisfied(result.requestId, 300)
|
|
464
|
+
return result
|
|
395
465
|
|
|
396
466
|
def read(self, id: str) -> KcResourceReadResponse:
|
|
397
467
|
"""Read a single resource by id.
|
|
@@ -499,9 +569,8 @@ class KcResourceClient:
|
|
|
499
569
|
"""Poll a change request once per second until it finishes or times out.
|
|
500
570
|
|
|
501
571
|
Returns as soon as the request succeeds (`succeeded == True`) or fails
|
|
502
|
-
(
|
|
503
|
-
|
|
504
|
-
the cases apart.
|
|
572
|
+
(`errorCode` set). On timeout it returns the last (still-pending) status
|
|
573
|
+
rather than raising — inspect `succeeded` to tell the cases apart.
|
|
505
574
|
|
|
506
575
|
Args:
|
|
507
576
|
request_id: the `requestId` to poll.
|
|
@@ -513,7 +582,21 @@ class KcResourceClient:
|
|
|
513
582
|
result = self.read_request(request_id)
|
|
514
583
|
|
|
515
584
|
i = 0
|
|
516
|
-
|
|
585
|
+
# Keep polling past a non-None errorMessage as long as errorCode is still None - the
|
|
586
|
+
# reconciler routinely stamps a benign, expected "please wait" errorMessage (e.g.
|
|
587
|
+
# "Temporary error. Waiting for all changes to finish first.") on EVERY throttled/gated
|
|
588
|
+
# retry tick while a resource is still legitimately in progress (see the various
|
|
589
|
+
# CloudCancelReconciliationException call sites across avant.cloud's reconcilers) -
|
|
590
|
+
# errorCode is what actually distinguishes a terminal failure. The old condition here
|
|
591
|
+
# (`result.errorMessage == None`) stopped polling and returned `succeeded=False` the
|
|
592
|
+
# moment ANY transient message appeared, without checking errorCode at all. Confirmed
|
|
593
|
+
# live via ES logs 2026-09-21 (Kubernetes E2E SDK test): the loop exited on
|
|
594
|
+
# `{"succeeded":false,"errorMessage":"Temporary error. Waiting for all changes to finish
|
|
595
|
+
# first.","errorCode":null}` mid-reconcile, and the caller (DocGuideExamples' Kubernetes
|
|
596
|
+
# example, and any real caller that doesn't separately check `.succeeded`) proceeded to
|
|
597
|
+
# read a resource that hadn't actually finished yet - e.g. status.kubeconfig was still
|
|
598
|
+
# None, crashing `f.write(kubeconfig)`.
|
|
599
|
+
while result.succeeded == False and result.errorCode == None:
|
|
517
600
|
i = i + 1
|
|
518
601
|
if i > timeout_seconds:
|
|
519
602
|
return result # timed out while still pending
|
|
@@ -523,12 +606,16 @@ class KcResourceClient:
|
|
|
523
606
|
|
|
524
607
|
if result.succeeded == True:
|
|
525
608
|
return result # applied successfully
|
|
526
|
-
if result.
|
|
609
|
+
if result.errorCode != None:
|
|
527
610
|
return result # failed terminally
|
|
528
611
|
|
|
529
612
|
return result
|
|
530
613
|
|
|
531
|
-
def create_or_update(
|
|
614
|
+
def create_or_update(
|
|
615
|
+
self,
|
|
616
|
+
data: dict,
|
|
617
|
+
submit_lock_retry_budget_seconds: float = _DEFAULT_LOCK_BUSY_RETRY_BUDGET_SECONDS,
|
|
618
|
+
) -> KcResourceCreateResponse:
|
|
532
619
|
"""Create a resource, or update it if one with the same id already exists.
|
|
533
620
|
|
|
534
621
|
Idempotent on the resource id: if no id is present in `data` (neither
|
|
@@ -540,6 +627,13 @@ class KcResourceClient:
|
|
|
540
627
|
data: the resource body, either the kubectl-style envelope
|
|
541
628
|
({"metadata": {...}, "spec": {...}}) or the flat shape. **Mutated
|
|
542
629
|
in place** to inject the generated id when absent.
|
|
630
|
+
submit_lock_retry_budget_seconds: how long to keep retrying a
|
|
631
|
+
SubmitLockBusy response (the per-organization submit lock losing a
|
|
632
|
+
concurrent-submit race - safe to retry the identical request, see
|
|
633
|
+
`KcApiModificationErrorCode.SubmitLockBusy`) before giving up and
|
|
634
|
+
returning it as-is. Default ~20min, matching kc_svc_job.py's own
|
|
635
|
+
retry budget for this same error. Pass 0 to disable retrying and get
|
|
636
|
+
the immediate fire-once behavior back.
|
|
543
637
|
|
|
544
638
|
Returns:
|
|
545
639
|
KcResourceCreateResponse with `requestId`/`resourceId`, or
|
|
@@ -565,19 +659,24 @@ class KcResourceClient:
|
|
|
565
659
|
|
|
566
660
|
url = f"{self.__api_url}/api/v1/{self.__resource_type}"
|
|
567
661
|
|
|
568
|
-
|
|
662
|
+
def _do_put():
|
|
663
|
+
response = create_http_client_with_retries(verify_ssl=self.__verify_ssl).put(url, json=data, headers=self.__headers())
|
|
664
|
+
logger.debug(
|
|
665
|
+
f"Got {response.status_code} status code while making request PUT {url}\nRequest body: {data}\nResponse body: {response.text}",
|
|
666
|
+
extra=self.__log_extra,
|
|
667
|
+
)
|
|
668
|
+
if response.status_code not in _HANDLED_STATUS_CODES:
|
|
669
|
+
raise Exception(
|
|
670
|
+
f"Got {response.status_code} status code while making request PUT {url}\nRequest body: {data}\nResponse body: {response.text}"
|
|
671
|
+
)
|
|
672
|
+
return response, KcResourceCreateResponse.Schema().load(response.json())
|
|
569
673
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
674
|
+
_, result = _retry_while_lock_busy(
|
|
675
|
+
_do_put,
|
|
676
|
+
lambda r: r.errorCode == KcApiModificationErrorCode.SubmitLockBusy,
|
|
677
|
+
submit_lock_retry_budget_seconds,
|
|
573
678
|
)
|
|
574
|
-
|
|
575
|
-
if response.status_code in _HANDLED_STATUS_CODES:
|
|
576
|
-
return KcResourceCreateResponse.Schema().load(response.json())
|
|
577
|
-
else:
|
|
578
|
-
raise Exception(
|
|
579
|
-
f"Got {response.status_code} status code while making request PUT {url}\nRequest body: {data}\nResponse body: {response.text}"
|
|
580
|
-
)
|
|
679
|
+
return result
|
|
581
680
|
|
|
582
681
|
def create(self, data: dict) -> KcResourceCreateResponse:
|
|
583
682
|
"""Left for compatibility! Use create_or_update instead.
|
|
@@ -587,13 +686,18 @@ class KcResourceClient:
|
|
|
587
686
|
"""
|
|
588
687
|
return self.create_or_update(data)
|
|
589
688
|
|
|
590
|
-
def update(
|
|
689
|
+
def update(
|
|
690
|
+
self,
|
|
691
|
+
data: dict,
|
|
692
|
+
submit_lock_retry_budget_seconds: float = _DEFAULT_LOCK_BUSY_RETRY_BUDGET_SECONDS,
|
|
693
|
+
) -> KcResourceUpdateResponse:
|
|
591
694
|
"""Left for compatibility! Use create_or_update instead.
|
|
592
695
|
|
|
593
696
|
Args:
|
|
594
697
|
data: see `create_or_update`.
|
|
698
|
+
submit_lock_retry_budget_seconds: see `create_or_update`.
|
|
595
699
|
"""
|
|
596
|
-
return self.create_or_update(data)
|
|
700
|
+
return self.create_or_update(data, submit_lock_retry_budget_seconds)
|
|
597
701
|
|
|
598
702
|
|
|
599
703
|
class KcClient:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kc-sdk-python
|
|
3
|
-
Version: 5.3.
|
|
3
|
+
Version: 5.3.2
|
|
4
4
|
Summary: Kvindo Cloud Python SDK — typed client for managing Kvindo Cloud infrastructure (VMs, S3, Kubernetes, load balancers, VPCs, PostgreSQL) via the REST API
|
|
5
5
|
Author: Kvindo
|
|
6
6
|
License-Expression: MIT
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "kc-sdk-python"
|
|
7
|
-
version = "5.3.
|
|
7
|
+
version = "5.3.2"
|
|
8
8
|
description = "Kvindo Cloud Python SDK — typed client for managing Kvindo Cloud infrastructure (VMs, S3, Kubernetes, load balancers, VPCs, PostgreSQL) via the REST API"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.8"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|