diff --git a/infisical_sdk/api_types.py b/infisical_sdk/api_types.py index 467673e..2b0fe29 100644 --- a/infisical_sdk/api_types.py +++ b/infisical_sdk/api_types.py @@ -194,3 +194,102 @@ class KmsKeyEncryptDataResponse(BaseModel): class KmsKeyDecryptDataResponse(BaseModel): """Response model for decrypt data API""" plaintext: str + +@dataclass +class CreateFolderResponseItem(BaseModel): + """Folder model with path for create response""" + id: str + name: str + createdAt: str + updatedAt: str + envId: str + path: str + version: Optional[int] = 1 + parentId: Optional[str] = None + isReserved: Optional[bool] = False + description: Optional[str] = None + lastSecretModified: Optional[str] = None + +@dataclass +class CreateFolderResponse(BaseModel): + """Response model for create folder API""" + folder: CreateFolderResponseItem + + @classmethod + def from_dict(cls, data: Dict) -> 'CreateFolderResponse': + return cls( + folder=CreateFolderResponseItem.from_dict(data['folder']), + ) + + +@dataclass +class ListFoldersResponseItem(BaseModel): + """Response model for list folders API""" + id: str + name: str + createdAt: str + updatedAt: str + envId: str + version: Optional[int] = 1 + parentId: Optional[str] = None + isReserved: Optional[bool] = False + description: Optional[str] = None + lastSecretModified: Optional[str] = None + relativePath: Optional[str] = None + + +@dataclass +class ListFoldersResponse(BaseModel): + """Complete response model for folders API""" + folders: List[ListFoldersResponseItem] + + @classmethod + def from_dict(cls, data: Dict) -> 'ListFoldersResponse': + """Create model from dictionary with camelCase keys, handling nested objects""" + return cls( + folders=[ListFoldersResponseItem.from_dict(folder) for folder in data['folders']] + ) + + +@dataclass +class Environment(BaseModel): + """Environment model""" + envId: str + envName: str + envSlug: str + +@dataclass +class SingleFolderResponseItem(BaseModel): + """Response model for get folder API""" + id: str + name: str + createdAt: str + updatedAt: str + envId: str + path: str + projectId: str + environment: Environment + version: Optional[int] = 1 + parentId: Optional[str] = None + isReserved: Optional[bool] = False + description: Optional[str] = None + lastSecretModified: Optional[str] = None + + @classmethod + def from_dict(cls, data: Dict) -> 'SingleFolderResponseItem': + """Create model from dictionary with nested Environment""" + folder_data = data.copy() + folder_data['environment'] = Environment.from_dict(data['environment']) + + return super().from_dict(folder_data) + +@dataclass +class SingleFolderResponse(BaseModel): + """Response model for get/create folder API""" + folder: SingleFolderResponseItem + + @classmethod + def from_dict(cls, data: Dict) -> 'SingleFolderResponse': + return cls( + folder=SingleFolderResponseItem.from_dict(data['folder']), + ) diff --git a/infisical_sdk/client.py b/infisical_sdk/client.py index 9913f12..1a91c2d 100644 --- a/infisical_sdk/client.py +++ b/infisical_sdk/client.py @@ -3,6 +3,7 @@ from infisical_sdk.resources import Auth from infisical_sdk.resources import V3RawSecrets from infisical_sdk.resources import KMS +from infisical_sdk.resources import V2Folders from infisical_sdk.util import SecretsCache @@ -24,6 +25,7 @@ def __init__(self, host: str, token: str = None, cache_ttl: int = 60): self.auth = Auth(self.api, self.set_token) self.secrets = V3RawSecrets(self.api, self.cache) self.kms = KMS(self.api) + self.folders = V2Folders(self.api) def set_token(self, token: str): """ diff --git a/infisical_sdk/resources/__init__.py b/infisical_sdk/resources/__init__.py index ee1bcb2..77a23f5 100644 --- a/infisical_sdk/resources/__init__.py +++ b/infisical_sdk/resources/__init__.py @@ -1,3 +1,4 @@ from .secrets import V3RawSecrets from .kms import KMS -from .auth import Auth \ No newline at end of file +from .auth import Auth +from .folders import V2Folders \ No newline at end of file diff --git a/infisical_sdk/resources/folders.py b/infisical_sdk/resources/folders.py new file mode 100644 index 0000000..519bf8b --- /dev/null +++ b/infisical_sdk/resources/folders.py @@ -0,0 +1,75 @@ +from typing import Optional +from datetime import datetime, timezone + +from infisical_sdk.infisical_requests import InfisicalRequests +from infisical_sdk.api_types import ListFoldersResponse, SingleFolderResponse, SingleFolderResponseItem, CreateFolderResponse, CreateFolderResponseItem + + +class V2Folders: + def __init__(self, requests: InfisicalRequests) -> None: + self.requests = requests + + def create_folder( + self, + name: str, + environment_slug: str, + project_id: str, + path: str = "/", + description: Optional[str] = None) -> CreateFolderResponseItem: + + request_body = { + "projectId": project_id, + "environment": environment_slug, + "name": name, + "path": path, + "description": description, + } + + result = self.requests.post( + path="/api/v2/folders", + json=request_body, + model=CreateFolderResponse + ) + + return result.data.folder + + def list_folders( + self, + project_id: str, + environment_slug: str, + path: str, + last_secret_modified: Optional[datetime] = None, + recursive: bool = False) -> ListFoldersResponse: + + params = { + "projectId": project_id, + "environment": environment_slug, + "path": path, + "recursive": recursive, + } + + if last_secret_modified is not None: + # Convert to UTC and format as RFC 3339 with 'Z' suffix + # The API expects UTC times in 'Z' format (e.g., 2023-11-07T05:31:56Z) + utc_datetime = last_secret_modified.astimezone(timezone.utc) if last_secret_modified.tzinfo else last_secret_modified.replace(tzinfo=timezone.utc) + params["lastSecretModified"] = utc_datetime.strftime('%Y-%m-%dT%H:%M:%SZ') + + result = self.requests.get( + path="/api/v2/folders", + params=params, + model=ListFoldersResponse + ) + + return result.data + + def get_folder_by_id( + self, + id: str) -> SingleFolderResponseItem: + + result = self.requests.get( + path=f"/api/v2/folders/{id}", + model=SingleFolderResponse + ) + + return result.data.folder +