@@ -2473,6 +2473,19 @@ def __repr__(self) -> str:
24732473FULL_ACCESS = _FullAccess ()
24742474
24752475
2476+ class _ClearValue :
2477+ """Sentinel for "send JSON ``null``" on fields where ``None`` means "leave unchanged".
2478+
2479+ ``update_api_key(..., rotation_due_at=CLEAR)`` clears the key's rotation due date.
2480+ """
2481+
2482+ def __repr__ (self ) -> str :
2483+ return "CLEAR"
2484+
2485+
2486+ CLEAR = _ClearValue ()
2487+
2488+
24762489def list_api_keys (
24772490 api_key : str ,
24782491 workspace_url : str ,
@@ -2516,6 +2529,8 @@ def create_api_key(
25162529 folder_ids : Optional [List [str ]] = None ,
25172530 custom_metadata : Optional [Dict ] = None ,
25182531 protected : bool = False ,
2532+ rotation_due_at : Optional [str ] = None ,
2533+ owners : Optional [List [str ]] = None ,
25192534) -> dict :
25202535 """POST /{workspace}/api-keys — create a new API key.
25212536
@@ -2524,8 +2539,11 @@ def create_api_key(
25242539 own scopes, so a full-access credential creates a full-access key. Pass a list
25252540 to scope the key (``role:<name>`` presets are accepted), ``[]`` for a key with
25262541 no abilities, or ``FULL_ACCESS`` to send an explicit ``null`` (unscoped/full
2527- access). ``scopes``, ``folder_ids``, and ``custom_metadata`` require the
2528- Advanced API Keys plan feature — the backend returns 403 if unavailable.
2542+ access). ``rotation_due_at`` (ISO date or date-time, UTC, in the future) and
2543+ ``owners`` (member ids reminded before it; ``[]`` means everyone who can roll
2544+ the key) set the rotation schedule. ``scopes``, ``folder_ids``,
2545+ ``custom_metadata``, ``rotation_due_at`` and ``owners`` require the Advanced
2546+ API Keys plan feature — the backend returns 403 if unavailable.
25292547 """
25302548 body : Dict [str , Any ] = {}
25312549 if name is not None :
@@ -2541,6 +2559,10 @@ def create_api_key(
25412559 body ["customMetadata" ] = custom_metadata
25422560 if protected :
25432561 body ["protected" ] = True
2562+ if rotation_due_at is not None :
2563+ body ["rotationDueAt" ] = rotation_due_at
2564+ if owners is not None :
2565+ body ["owners" ] = owners
25442566 response = http .post (f"{ API_URL } /{ workspace_url } /api-keys" , params = {"api_key" : api_key }, json = body )
25452567 if not response .ok :
25462568 raise RoboflowError (response .text , status_code = response .status_code )
@@ -2551,21 +2573,24 @@ def update_api_key(api_key: str, workspace_url: str, key_id: str, **fields: Any)
25512573 """PATCH /{workspace}/api-keys/{keyId} — update an existing API key.
25522574
25532575 Pass only the fields you want to change as keyword arguments:
2554- ``name``, ``scopes``, ``custom_metadata``, ``protected``, ``disabled``.
2576+ ``name``, ``scopes``, ``custom_metadata``, ``protected``, ``disabled``,
2577+ ``rotation_due_at``, ``owners``.
25552578 ``None`` values are omitted (left unchanged). To send explicit values,
25562579 pass ``scopes=[]`` (no abilities), ``scopes=FULL_ACCESS`` (unscoped/full
2557- access, serialized as ``null``), or ``custom_metadata={}`` (clear metadata).
2580+ access, serialized as ``null``), ``custom_metadata={}`` (clear metadata),
2581+ ``rotation_due_at=CLEAR`` (clear the due date, serialized as ``null``) or
2582+ ``owners=[]`` (remind everyone who can roll the key).
25582583 The API cannot unprotect a key (``protected=False`` → 403).
25592584 Disabling a protected key returns 409.
25602585 """
25612586 encoded = quote (key_id , safe = "" )
25622587 # Canonical wire field is camelCase `customMetadata` (consistent with `folderIds`); callers may
25632588 # pass the Pythonic `custom_metadata` kwarg, which is normalized here.
2564- wire_aliases = {"custom_metadata" : "customMetadata" }
2589+ wire_aliases = {"custom_metadata" : "customMetadata" , "rotation_due_at" : "rotationDueAt" }
25652590 body : Dict [str , Any ] = {}
25662591 for k , v in fields .items ():
25672592 wire_key = wire_aliases .get (k , k )
2568- if v is FULL_ACCESS :
2593+ if v is FULL_ACCESS or v is CLEAR :
25692594 body [wire_key ] = None
25702595 elif v is not None :
25712596 body [wire_key ] = v
@@ -2592,3 +2617,36 @@ def revoke_api_key(api_key: str, workspace_url: str, key_id: str) -> dict:
25922617 if not response .ok :
25932618 raise RoboflowError (response .text , status_code = response .status_code )
25942619 return response .json ()
2620+
2621+
2622+ def roll_api_key (
2623+ api_key : str ,
2624+ workspace_url : str ,
2625+ key_id : str ,
2626+ rotation_due_at : Optional [str ] = None ,
2627+ owners : Optional [List [str ]] = None ,
2628+ ) -> dict :
2629+ """POST /{workspace}/api-keys/{keyId}/roll — revoke a key and mint its replacement.
2630+
2631+ The replacement keeps the old key's name, scopes, folders, metadata, protection
2632+ and disabled state, and its secret ``key`` value is returned only here (shown
2633+ once). ``rotation_due_at`` (ISO date or date-time, UTC, in the future) and
2634+ ``owners`` set the replacement's rotation schedule; omitted, the old key's
2635+ schedule carries over. Both require the Advanced API Keys plan feature.
2636+ Rolling a protected or already rolled key returns 409; a credential that holds
2637+ less access than the target key gets 403.
2638+ """
2639+ encoded = quote (key_id , safe = "" )
2640+ body : Dict [str , Any ] = {}
2641+ if rotation_due_at is not None :
2642+ body ["rotationDueAt" ] = rotation_due_at
2643+ if owners is not None :
2644+ body ["owners" ] = owners
2645+ response = http .post (
2646+ f"{ API_URL } /{ workspace_url } /api-keys/{ encoded } /roll" ,
2647+ params = {"api_key" : api_key },
2648+ json = body or None ,
2649+ )
2650+ if not response .ok :
2651+ raise RoboflowError (response .text , status_code = response .status_code )
2652+ return response .json ()
0 commit comments