kc-sdk-python 5.3.0__tar.gz → 5.3.1__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.1}/PKG-INFO +1 -1
- {kc_sdk_python-5.3.0 → kc_sdk_python-5.3.1}/kc_api.py +122 -31
- {kc_sdk_python-5.3.0 → kc_sdk_python-5.3.1/kc_sdk_python.egg-info}/PKG-INFO +1 -1
- {kc_sdk_python-5.3.0 → kc_sdk_python-5.3.1}/pyproject.toml +1 -1
- {kc_sdk_python-5.3.0 → kc_sdk_python-5.3.1}/LICENSE +0 -0
- {kc_sdk_python-5.3.0 → kc_sdk_python-5.3.1}/README.md +0 -0
- {kc_sdk_python-5.3.0 → kc_sdk_python-5.3.1}/kc_sdk_python.egg-info/SOURCES.txt +0 -0
- {kc_sdk_python-5.3.0 → kc_sdk_python-5.3.1}/kc_sdk_python.egg-info/dependency_links.txt +0 -0
- {kc_sdk_python-5.3.0 → kc_sdk_python-5.3.1}/kc_sdk_python.egg-info/requires.txt +0 -0
- {kc_sdk_python-5.3.0 → kc_sdk_python-5.3.1}/kc_sdk_python.egg-info/top_level.txt +0 -0
- {kc_sdk_python-5.3.0 → kc_sdk_python-5.3.1}/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.1
|
|
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.
|
|
@@ -528,7 +598,11 @@ class KcResourceClient:
|
|
|
528
598
|
|
|
529
599
|
return result
|
|
530
600
|
|
|
531
|
-
def create_or_update(
|
|
601
|
+
def create_or_update(
|
|
602
|
+
self,
|
|
603
|
+
data: dict,
|
|
604
|
+
submit_lock_retry_budget_seconds: float = _DEFAULT_LOCK_BUSY_RETRY_BUDGET_SECONDS,
|
|
605
|
+
) -> KcResourceCreateResponse:
|
|
532
606
|
"""Create a resource, or update it if one with the same id already exists.
|
|
533
607
|
|
|
534
608
|
Idempotent on the resource id: if no id is present in `data` (neither
|
|
@@ -540,6 +614,13 @@ class KcResourceClient:
|
|
|
540
614
|
data: the resource body, either the kubectl-style envelope
|
|
541
615
|
({"metadata": {...}, "spec": {...}}) or the flat shape. **Mutated
|
|
542
616
|
in place** to inject the generated id when absent.
|
|
617
|
+
submit_lock_retry_budget_seconds: how long to keep retrying a
|
|
618
|
+
SubmitLockBusy response (the per-organization submit lock losing a
|
|
619
|
+
concurrent-submit race - safe to retry the identical request, see
|
|
620
|
+
`KcApiModificationErrorCode.SubmitLockBusy`) before giving up and
|
|
621
|
+
returning it as-is. Default ~20min, matching kc_svc_job.py's own
|
|
622
|
+
retry budget for this same error. Pass 0 to disable retrying and get
|
|
623
|
+
the immediate fire-once behavior back.
|
|
543
624
|
|
|
544
625
|
Returns:
|
|
545
626
|
KcResourceCreateResponse with `requestId`/`resourceId`, or
|
|
@@ -565,19 +646,24 @@ class KcResourceClient:
|
|
|
565
646
|
|
|
566
647
|
url = f"{self.__api_url}/api/v1/{self.__resource_type}"
|
|
567
648
|
|
|
568
|
-
|
|
649
|
+
def _do_put():
|
|
650
|
+
response = create_http_client_with_retries(verify_ssl=self.__verify_ssl).put(url, json=data, headers=self.__headers())
|
|
651
|
+
logger.debug(
|
|
652
|
+
f"Got {response.status_code} status code while making request PUT {url}\nRequest body: {data}\nResponse body: {response.text}",
|
|
653
|
+
extra=self.__log_extra,
|
|
654
|
+
)
|
|
655
|
+
if response.status_code not in _HANDLED_STATUS_CODES:
|
|
656
|
+
raise Exception(
|
|
657
|
+
f"Got {response.status_code} status code while making request PUT {url}\nRequest body: {data}\nResponse body: {response.text}"
|
|
658
|
+
)
|
|
659
|
+
return response, KcResourceCreateResponse.Schema().load(response.json())
|
|
569
660
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
661
|
+
_, result = _retry_while_lock_busy(
|
|
662
|
+
_do_put,
|
|
663
|
+
lambda r: r.errorCode == KcApiModificationErrorCode.SubmitLockBusy,
|
|
664
|
+
submit_lock_retry_budget_seconds,
|
|
573
665
|
)
|
|
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
|
-
)
|
|
666
|
+
return result
|
|
581
667
|
|
|
582
668
|
def create(self, data: dict) -> KcResourceCreateResponse:
|
|
583
669
|
"""Left for compatibility! Use create_or_update instead.
|
|
@@ -587,13 +673,18 @@ class KcResourceClient:
|
|
|
587
673
|
"""
|
|
588
674
|
return self.create_or_update(data)
|
|
589
675
|
|
|
590
|
-
def update(
|
|
676
|
+
def update(
|
|
677
|
+
self,
|
|
678
|
+
data: dict,
|
|
679
|
+
submit_lock_retry_budget_seconds: float = _DEFAULT_LOCK_BUSY_RETRY_BUDGET_SECONDS,
|
|
680
|
+
) -> KcResourceUpdateResponse:
|
|
591
681
|
"""Left for compatibility! Use create_or_update instead.
|
|
592
682
|
|
|
593
683
|
Args:
|
|
594
684
|
data: see `create_or_update`.
|
|
685
|
+
submit_lock_retry_budget_seconds: see `create_or_update`.
|
|
595
686
|
"""
|
|
596
|
-
return self.create_or_update(data)
|
|
687
|
+
return self.create_or_update(data, submit_lock_retry_budget_seconds)
|
|
597
688
|
|
|
598
689
|
|
|
599
690
|
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.1
|
|
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.1"
|
|
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
|